You are viewing a plain text version of this content. The canonical link for it is here.
Posted to oak-dev@jackrabbit.apache.org by "shodaaan (via GitHub)" <gi...@apache.org> on 2024/10/17 09:05:36 UTC

[PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

shodaaan opened a new pull request, #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805

   - created GenerateFullGCCommand oak-run class with methods for generating garbage of type GAP_ORPHANS and EMPTY_PROPERTIES. All garbage is generated under a specific main node. Also created a cleanup method for deleting all generated garbage
   - added call to GenerateFullGCCommand to AvailableModes with specific oak-run input variable
   - created GenerateFullGCCommandTest class with unit test methods for generating and cleaning up garbage
   - made VersionGarbageCollector.fullGCMode public, so it can be used Command class
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "shodaaan (via GitHub)" <gi...@apache.org>.
shodaaan commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1812638904


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateGarbageCommand.java:
##########
@@ -0,0 +1,427 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.commons.concurrent.ExecutorCloser;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.GenerateGarbageHelper;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateGarbageCommand implements Command, Closeable {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateGarbageCommand.class);
+
+    private final ScheduledExecutorService continuousRunExecutor = Executors.newScheduledThreadPool(1);
+
+    private static final String USAGE = "createGarbage {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * FullGC garbage should be generated under tmp node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH_BASE = "tmp";
+
+    /**
+     * The root node name for fullGC garbage generation, one level under tmp.
+     */
+    public static String GARBAGE_GEN_ROOT_NODE_NAME = "fullGC_GarbageRoot";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH = GARBAGE_GEN_ROOT_PATH_BASE + "/" + GARBAGE_GEN_ROOT_NODE_NAME;
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String GEN_BASE_PATH = "GenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String GEN_PARENT_NODE_PREFIX = "Parent_";
+    public static String GEN_NODE_PREFIX = "Node_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node FULLGC_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public GenerateFullGCOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public GenerateFullGCOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {
+
+        try (Closer closer = Closer.create()) {
+            execute(closer, args);
+        } catch (Throwable e) {
+            LOG.error("Command failed", e);
+            throw e;
+        }
+    }
+
+    /**
+     * Method with passed closer is necessary in order to allow for unit tests to check the output of the command.
+     * It is the responsibility of the caller to close the closer.
+     *
+     * Returns the list of generated garbage base paths (under the garbage root node).
+     * @param closer
+     * @param args
+     * @throws Exception
+     */
+    public List<String> execute(Closer closer, String... args) throws Exception {
+        continuousRunIndex = 0;
+
+        List<String> generateBasePaths = new ArrayList<>();
+
+        GenerateFullGCOptions options = new GenerateFullGCOptions(USAGE).parse(args);
+        String subCmd = options.getSubCmd();
+
+        if (GenerateFullGCOptions.CMD_GENERATE.equals(subCmd)) {
+
+            if (options.getCreateGarbageNodesCount() <= 0) {
+                LOG.error("Invalid garbageNodesCount specified: " + options.getCreateGarbageNodesCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+            if (options.getGarbageNodesParentCount() <= 0) {
+                LOG.error("Invalid garbageNodesParentCount specified: " + options.getGarbageNodesParentCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+
+            if (options.getNumberOfRuns() > 1 && options.getGenerateIntervalSeconds() > 0) {
+                generateBasePaths.addAll(generateGarbageContinuously(options, closer));
+            } else {
+                generateBasePaths.add(generateGarbage(options, closer, 0));
+            }
+        } else if (GenerateFullGCOptions.CMD_CLEAN.equals(subCmd)) {
+            cleanGarbage(options, closer);
+        } else {
+            System.err.println("Unknown revisions command: " + subCmd);
+        }
+
+        return generateBasePaths;
+    }
+
+    @Override
+    public void close() throws IOException {
+        new ExecutorCloser(this.continuousRunExecutor).close();
+    }
+
+    private List<String> generateGarbageContinuously(GenerateFullGCOptions options, Closer closer) throws Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long startGenTimestamp = System.currentTimeMillis();
+
+        List<String> generatedGarbageBasePaths = new ArrayList<>();
+
+        int numberOfRuns = options.getNumberOfRuns();
+        int intervalSeconds = options.getGenerateIntervalSeconds();
+        Runnable task = () -> {
+            if (continuousRunIndex < numberOfRuns) {
+                try {
+                    String genBasePath = generateGarbage(options, closer, continuousRunIndex, builder, startGenTimestamp);
+                    generatedGarbageBasePaths.add(genBasePath);
+                } catch (Exception e) {
+                    LOG.error("Error generating garbage in run " + continuousRunIndex, e);
+                }
+                LOG.info("Task executed. Count: " + (continuousRunIndex + 1));
+                continuousRunIndex++;
+            } else {
+                // Shutdown the executor once the task has run numberOfRuns times
+                continuousRunExecutor.shutdown();
+                LOG.info("Task completed " + numberOfRuns + " times. Stopping execution.");
+            }
+        };
+
+        // Schedule the task to run every intervalSeconds
+        continuousRunExecutor.scheduleAtFixedRate(task, 0, intervalSeconds, TimeUnit.SECONDS);
+
+        return generatedGarbageBasePaths;
+    }
+
+    /**
+     * Generate garbage nodes in the repository in order to allow for testing fullGC functionality.
+     *
+     * Returns the path of the generated FULLGC_GEN_BASE_PATH node (under the root).
+     * @param options
+     * @param closer
+     * @param runIndex
+     * @return
+     * @throws IOException
+     * @throws Exception
+     */
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long generationTimestamp = System.currentTimeMillis();
+
+        return generateGarbage(options, closer, runIndex, builder, generationTimestamp);
+    }
+
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex,
+                                  DocumentNodeStoreBuilder<?> builder, long timestamp) throws IOException, Exception {
+
+        if (builder == null) {
+            System.err.println("generateFullGC mode only available for DocumentNodeStore");
+            System.exit(1);
+        }
+
+        String generationBasePath = GEN_BASE_PATH + timestamp + "_" + runIndex;
+        System.out.println("Generating fullGC on the document: " + generationBasePath);
+        documentNodeStore = builder.build();
+
+        //VersionGarbageCollector.FullGCMode fullGCMode = getFullGCMode(options);
+        if (GenerateGarbageHelper.isInvalidGarbageGenerationMode(options.getGarbageType())) {
+            LOG.error("Invalid fullGCMode specified: " + options.getGarbageType() + " in: " + getClass().getName());
+            System.exit(1);
+        }
+
+        //1. Create nodes with properties
+        NodeBuilder rootNode = documentNodeStore.getRoot().builder();
+        NodeBuilder garbageRootNode = rootNode.child(GARBAGE_GEN_ROOT_PATH_BASE).child(GARBAGE_GEN_ROOT_NODE_NAME);
+        garbageRootNode.child(generationBasePath).setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+
+        int nodesCountUnderParent = options.getCreateGarbageNodesCount() / options.getGarbageNodesParentCount();
+        for(int i = 0; i < options.getGarbageNodesParentCount(); i ++) {
+            garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).setProperty(JcrConstants.JCR_PRIMARYTYPE, "nt:folder", NAME);
+
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                        setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+
+                if (GenerateGarbageHelper.includesEmptyProps(options.getGarbageType())) {
+                    garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                            setProperty(EMPTY_PROPERTY_NAME, "bar", STRING);
+                }
+            }
+        }
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+
+
+        //2. Generate garbage nodes - EMPTY_PROPERTIES
+        if (GenerateGarbageHelper.includesEmptyProps(options.getGarbageType())) {
+            for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+                for (int j = 0; j < nodesCountUnderParent; j++) {
+                    garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                            removeProperty(EMPTY_PROPERTY_NAME);
+                }
+            }
+        }
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+
+        //3.1. Generate garbage nodes - GAP_ORPHANS - remove parent nodes
+        if (GenerateGarbageHelper.includesGapOrphans(options.getGarbageType())) {

Review Comment:
   Thank you for the suggestion, this is a good idea for the future, when we will have multiple garbageTypes generated by the GenerateGarbageCommand.
   For now, since we only have 2 types, one if/else is short and quite readable.
   
   As suggested, I have completely separated the methods for generating EMPTY_PROPS / GAP_ORPHANS garbage.
   
   Will definitely revisit the rest of the suggestion when more garbageType generation methods are implemented.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "rishabhdaim (via GitHub)" <gi...@apache.org>.
rishabhdaim commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1804592380


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateFullGCCommand.java:
##########
@@ -0,0 +1,416 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.plugins.document.Collection;
+import org.apache.jackrabbit.oak.plugins.document.Document;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.DocumentStoreException;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateFullGCCommand implements Command {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateFullGCCommand.class);
+
+    private static final String USAGE = "generateFullGC {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String FULLGC_GEN_ROOT_PATH = "fullGCGenRoot";
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String FULLGC_GEN_BASE_PATH = "fullGCGenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String FULLGC_GEN_PARENT_NODE_PREFIX = "fullGCParent_";
+    public static String FULLGC_GEN_NODE_PREFIX = "fullGCNode_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node FULLGC_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public GenerateFullGCOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public GenerateFullGCOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {
+        Closer closer = Closer.create();
+        try {
+            execute(closer, args);
+        } catch (Throwable e) {
+            LOG.error("Command failed", e);
+            throw closer.rethrow(e);
+        } finally {
+            closer.close();
+        }
+    }
+
+    /**
+     * Method with passed closer is necessary in order to allow for unit tests to check the output of the command.
+     * It is the responsibility of the caller to close the closer.
+     *
+     * Returns the list of generated garbage base paths (under the garbage root node).
+     * @param closer
+     * @param args
+     * @throws Exception
+     */
+    public List<String> execute(Closer closer, String... args) throws Exception {
+        continuousRunIndex = 0;
+
+        List<String> generateBasePaths = new ArrayList<>();
+
+        GenerateFullGCOptions options = new GenerateFullGCOptions(USAGE).parse(args);
+        String subCmd = options.getSubCmd();
+
+        if (GenerateFullGCOptions.CMD_GENERATE.equals(subCmd)) {
+            if (options.getNumberOfRuns() > 1 && options.getGenerateIntervalSeconds() > 0) {
+                generateBasePaths.addAll(generateGarbageContinuously(options, closer));
+            } else {
+                generateBasePaths.add(generateGarbage(options, closer, 0));
+            }
+        } else if (GenerateFullGCOptions.CMD_CLEAN.equals(subCmd)) {
+            cleanGarbage(options, closer);
+        } else {
+            System.err.println("unknown revisions command: " + subCmd);
+        }
+
+        return generateBasePaths;
+    }
+
+    private List<String> generateGarbageContinuously(GenerateFullGCOptions options, Closer closer) throws IOException, Exception {
+        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);

Review Comment:
   Could you please close the executor once finished.
   See https://github.com/shodaaan/jackrabbit-oak/blob/trunk/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/mongo/MongoDocumentStoreThrottlingMetricsUpdater.java#L58 for reference.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "shodaaan (via GitHub)" <gi...@apache.org>.
shodaaan commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1809009813


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateFullGCCommand.java:
##########
@@ -0,0 +1,416 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.plugins.document.Collection;
+import org.apache.jackrabbit.oak.plugins.document.Document;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.DocumentStoreException;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateFullGCCommand implements Command {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateFullGCCommand.class);
+
+    private static final String USAGE = "generateFullGC {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String FULLGC_GEN_ROOT_PATH = "fullGCGenRoot";
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String FULLGC_GEN_BASE_PATH = "fullGCGenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String FULLGC_GEN_PARENT_NODE_PREFIX = "fullGCParent_";
+    public static String FULLGC_GEN_NODE_PREFIX = "fullGCNode_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node FULLGC_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public GenerateFullGCOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public GenerateFullGCOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {
+        Closer closer = Closer.create();
+        try {
+            execute(closer, args);
+        } catch (Throwable e) {
+            LOG.error("Command failed", e);
+            throw closer.rethrow(e);
+        } finally {
+            closer.close();
+        }
+    }
+
+    /**
+     * Method with passed closer is necessary in order to allow for unit tests to check the output of the command.
+     * It is the responsibility of the caller to close the closer.
+     *
+     * Returns the list of generated garbage base paths (under the garbage root node).
+     * @param closer
+     * @param args
+     * @throws Exception
+     */
+    public List<String> execute(Closer closer, String... args) throws Exception {
+        continuousRunIndex = 0;
+
+        List<String> generateBasePaths = new ArrayList<>();
+
+        GenerateFullGCOptions options = new GenerateFullGCOptions(USAGE).parse(args);
+        String subCmd = options.getSubCmd();
+
+        if (GenerateFullGCOptions.CMD_GENERATE.equals(subCmd)) {
+            if (options.getNumberOfRuns() > 1 && options.getGenerateIntervalSeconds() > 0) {
+                generateBasePaths.addAll(generateGarbageContinuously(options, closer));
+            } else {
+                generateBasePaths.add(generateGarbage(options, closer, 0));
+            }
+        } else if (GenerateFullGCOptions.CMD_CLEAN.equals(subCmd)) {
+            cleanGarbage(options, closer);
+        } else {
+            System.err.println("unknown revisions command: " + subCmd);
+        }
+
+        return generateBasePaths;
+    }
+
+    private List<String> generateGarbageContinuously(GenerateFullGCOptions options, Closer closer) throws IOException, Exception {
+        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long startGenTimestamp = System.currentTimeMillis();
+
+        List<String> generatedGarbageBasePaths = new ArrayList<>();
+
+        int numberOfRuns = options.getNumberOfRuns();
+        int intervalSeconds = options.getGenerateIntervalSeconds();
+        Runnable task = () -> {
+            if (continuousRunIndex < numberOfRuns) {
+                try {
+                    String genBasePath = generateGarbage(options, closer, continuousRunIndex, builder, startGenTimestamp);
+                    generatedGarbageBasePaths.add(genBasePath);
+                } catch (Exception e) {
+                    LOG.error("Error generating garbage in run " + continuousRunIndex, e);
+                }
+                LOG.info("Task executed. Count: " + (continuousRunIndex + 1));
+                continuousRunIndex++;
+            } else {
+                // Shutdown the executor once the task has run numberOfRuns times
+                executor.shutdown();
+                LOG.info("Task completed " + numberOfRuns + " times. Stopping execution.");
+            }
+        };
+
+        // Schedule the task to run every intervalSeconds
+        executor.scheduleAtFixedRate(task, 0, intervalSeconds, TimeUnit.SECONDS);
+
+        return generatedGarbageBasePaths;
+    }
+
+    /**
+     * Generate garbage nodes in the repository in order to allow for testing fullGC functionality.
+     *
+     * Returns the path of the generated FULLGC_GEN_BASE_PATH node (under the root).
+     * @param options
+     * @param closer
+     * @param runIndex
+     * @return
+     * @throws IOException
+     * @throws Exception
+     */
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long generationTimestamp = System.currentTimeMillis();
+
+        return generateGarbage(options, closer, runIndex, builder, generationTimestamp);
+    }
+
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex,
+                                  DocumentNodeStoreBuilder<?> builder, long timestamp) throws IOException, Exception {
+
+        if (builder == null) {
+            System.err.println("generateFullGC mode only available for DocumentNodeStore");
+            System.exit(1);
+        }
+
+        String generationBasePath = FULLGC_GEN_BASE_PATH + timestamp + "_" + runIndex;
+        System.out.println("Generating fullGC on the document: " + generationBasePath);
+        documentNodeStore = builder.build();
+
+        VersionGarbageCollector.FullGCMode fullGCMode = getFullGCMode(options);
+        if (fullGCMode == VersionGarbageCollector.FullGCMode.NONE) {
+            LOG.error("Invalid garbageType specified. Must be one of the following: 1 - EMPTYPROPS, 2 - GAP_ORPHANS, 3 - GAP_ORPHANS_EMPTYPROPS");
+            System.exit(1);
+        }
+
+        //1. Create nodes with properties
+        NodeBuilder rootNode = documentNodeStore.getRoot().builder();
+        NodeBuilder garbageRootNode = rootNode.child(FULLGC_GEN_ROOT_PATH);
+        garbageRootNode.child(generationBasePath).setProperty("jcr:primaryType", "nt:file", NAME);
+
+        int nodesCountUnderParent = options.getCreateGarbageNodesCount() / options.getGarbageNodesParentCount();
+        for(int i = 0; i < options.getGarbageNodesParentCount(); i ++) {
+            garbageRootNode.child(generationBasePath).child(FULLGC_GEN_PARENT_NODE_PREFIX + i).setProperty("jcr:primaryType", "nt:folder", NAME);
+
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                garbageRootNode.child(generationBasePath).child(FULLGC_GEN_PARENT_NODE_PREFIX + i).child(FULLGC_GEN_NODE_PREFIX + j).
+                        setProperty("jcr:primaryType", "nt:file", NAME);
+
+                if (fullGCMode == VersionGarbageCollector.FullGCMode.EMPTYPROPS || fullGCMode == VersionGarbageCollector.FullGCMode.GAP_ORPHANS_EMPTYPROPS) {
+                    garbageRootNode.child(generationBasePath).child(FULLGC_GEN_PARENT_NODE_PREFIX + i).child(FULLGC_GEN_NODE_PREFIX + j).
+                            setProperty(EMPTY_PROPERTY_NAME, "bar", STRING);
+                }
+            }
+        }
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+
+
+        //2. Generate garbage nodes - EMPTY_PROPERTIES
+        if (fullGCMode == VersionGarbageCollector.FullGCMode.EMPTYPROPS) {
+            for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+                for (int j = 0; j < nodesCountUnderParent; j++) {
+                    garbageRootNode.child(generationBasePath).child(FULLGC_GEN_PARENT_NODE_PREFIX + i).child(FULLGC_GEN_NODE_PREFIX + j).
+                            removeProperty(EMPTY_PROPERTY_NAME);
+                }
+            }
+        }
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+
+        //3.1. Generate garbage nodes - GAP_ORPHANS - remove parent nodes
+        if (fullGCMode == VersionGarbageCollector.FullGCMode.GAP_ORPHANS) {
+            StringBuilder sbNodePath = new StringBuilder();
+            List<String> deleteNodePaths = new ArrayList<>();
+            for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+
+                sbNodePath.setLength(0);
+                sbNodePath.append("3:/").append(FULLGC_GEN_ROOT_PATH).append("/").append(generationBasePath).append("/").
+                        append(FULLGC_GEN_PARENT_NODE_PREFIX).append(i);
+                deleteNodePaths.add(sbNodePath.toString());
+            }
+            // Remove all parent nodes
+            documentNodeStore.getDocumentStore().remove(org.apache.jackrabbit.oak.plugins.document.Collection.NODES, deleteNodePaths);
+            documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+            documentNodeStore.runBackgroundOperations();
+        }
+
+        return generationBasePath;
+    }
+
+    /**
+     * Cleans up all generated garbage by removing the node FULLGC_GEN_ROOT_PATH and all of
+     * its children (recursively)
+     * @param options
+     * @param closer
+     * @throws IOException
+     * @throws Exception
+     */
+    private void cleanGarbage(GenerateFullGCOptions options, Closer closer) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+
+        if (builder == null) {
+            System.err.println("generateFullGC mode only available for DocumentNodeStore");
+            System.exit(1);
+        }
+
+        System.out.println("Cleaning up all generated garbage:");
+        documentNodeStore = builder.build();
+
+        NodeBuilder rootBuilder = documentNodeStore.getRoot().builder();
+
+        NodeBuilder generatedGarbageRootBuilder = rootBuilder.child(FULLGC_GEN_ROOT_PATH);
+
+        String garbageRootNodePath = "1:/"+FULLGC_GEN_ROOT_PATH;
+        List<String> childNodePaths = new ArrayList<>();
+        childNodePaths.add(garbageRootNodePath);
+
+        // get all paths of the tree nodes under the garbage root node
+        getTreeNodePaths(generatedGarbageRootBuilder, garbageRootNodePath, childNodePaths, 1);
+
+        documentNodeStore.getDocumentStore().remove(org.apache.jackrabbit.oak.plugins.document.Collection.NODES, childNodePaths);
+        documentNodeStore.merge(rootBuilder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+    }
+
+    /**
+     * Recursively get all paths of the tree nodes under the given root node.
+     * @param rootNode
+     * @param basePath
+     * @param treeNodePaths
+     * @param nodeLevel
+     */
+    private void getTreeNodePaths(NodeBuilder rootNode, String basePath,
+                                  List<String> treeNodePaths, int nodeLevel) {
+        String childBasePath = basePath.replaceFirst(nodeLevel + ":/", (nodeLevel + 1) + ":/");
+        for (String childNodeName : rootNode.getChildNodeNames()) {
+            String childPath = childBasePath + "/" + childNodeName;
+            treeNodePaths.add(childPath);
+            getTreeNodePaths(rootNode.child(childNodeName), childPath, treeNodePaths, nodeLevel + 1);
+        }
+    }
+
+    /**
+     * Used in unit tests for retrieving a document by path from the document store using the documentNodeStore
+     * of this class.
+     * @param collection
+     * @param key
+     * @param maxCacheAge
+     * @return
+     * @param <T>
+     * @throws DocumentStoreException
+     */
+    @Nullable
+    public <T extends Document> T getDocument(Collection<T> collection, String key, int maxCacheAge)

Review Comment:
   Done. Thank you for the suggestion.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "rishabhdaim (via GitHub)" <gi...@apache.org>.
rishabhdaim commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1831399984


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/CreateGarbageCommand.java:
##########
@@ -0,0 +1,574 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.base.Joiner;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.commons.concurrent.ExecutorCloser;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.GenerateGarbageHelper;
+import org.apache.jackrabbit.oak.plugins.document.LeaseCheckMode;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.asCloseable;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateGarbageCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class CreateGarbageCommand implements Command, Closeable {
+    private static final Logger LOG = LoggerFactory.getLogger(CreateGarbageCommand.class);
+
+    private final ScheduledExecutorService continuousRunExecutor = Executors.newScheduledThreadPool(1);
+
+    private static final String USAGE = Joiner.on(System.lineSeparator()).join(
+            "create-test-garbage {<jdbc-uri> | <mongodb-uri>} <sub-command> [options] ",
+            "where sub-command is one of: ",
+            "  clean - clean up all generated garbage under the specified root node",
+            "  generate - generate garbage nodes in the repository, under the root node tmp/oak-run-generated-test-garbage. ",
+            "           Use the --garbageType (required) option to specify the type of garbage to generate",
+            "           the --garbageNodesCount option to specify the total number of garbage nodes to create,",
+            "           the --garbageNodesParentCount option to specify the total number of parent nodes under which to create garbage nodes,",
+            "           the --orphansDepth option to specify the depth in the tree at which to create garbage nodes,",
+            "           the --orphansLevelGap option to specify the gap in the tree between the first leaf gap orphan garbage node and its parent,",
+            "           the --numberOfRuns option to specify the number of garbage generation runs to do,",
+            "           and the --generateIntervalSeconds option to specify the interval at which to generate a complete garbage count from createGarbageNotesCount.");
+
+
+    /**
+     * Garbage should be generated under tmp node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH_BASE = "tmp";
+
+    /**
+     * The root node name for garbage generation, one level under tmp.
+     */
+    public static String GARBAGE_GEN_ROOT_NODE_NAME = "oak-run-generated-test-garbage";
+
+    /**
+     * Root node for garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH = GARBAGE_GEN_ROOT_PATH_BASE + "/" + GARBAGE_GEN_ROOT_NODE_NAME;
+
+    /**
+     * Base path for garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String GEN_BASE_PATH = "GenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String GEN_PARENT_NODE_PREFIX = "Parent_";
+    public static String GEN_NODE_PREFIX = "Node_";
+    public static String GEN_NODE_LEVEL_PREFIX = "_Level_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    /**
+     * The maximum depth in the tree at which to generate gap orphans garbage nodes.
+     * Used for validation of the orphansDepth / orphansLevelGap options.
+     */
+    private final int GAP_ORPHANS_MAX_DEPTH = 15;
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class CreateGarbageOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node GARBAGE_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> orphansDepth;
+        final OptionSpec<Integer> orphansLevelGap;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public CreateGarbageOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            orphansDepth = parser
+                    .accepts("orphansDepth", "the depth in the tree at which to create garbage nodes. Only applies to ORPHANS fullGC modes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            orphansLevelGap = parser
+                    .accepts("orphansLevelGap", "the gap in the tree between the first leaf gap orphan garbage node and its' parent. Only applies to ORPHANS fullGC modes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public CreateGarbageOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getOrphansDepth() { return orphansDepth.value(options);}
+
+        public int getOrphansLevelGap() { return orphansLevelGap.value(options); }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {
+
+        try (Closer closer = Closer.create()) {
+            // Register the ExecutorService with Closer
+            closer.register(() -> {
+                // Properly shutdown the executor service
+                continuousRunExecutor.shutdown();
+                try {
+                    if (!continuousRunExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
+                        continuousRunExecutor.shutdownNow();
+                    }
+                } catch (InterruptedException e) {
+                    throw new RuntimeException(e);
+                }
+            });
+
+            execute(closer, args);
+
+        } catch (Throwable e) {
+            LOG.error("Command failed", e);
+            throw e;
+        }
+    }
+
+    /**
+     * Method with passed closer is necessary in order to allow for unit tests to check the output of the command.
+     * It is the responsibility of the caller to close the closer.
+     *
+     * Returns the list of generated garbage base paths (under the garbage root node).
+     * @param closer
+     * @param args
+     * @throws Exception
+     */
+    public List<String> execute(Closer closer, String... args) throws Exception {
+        continuousRunIndex = 0;
+
+        List<String> generateBasePaths = new ArrayList<>();
+
+        CreateGarbageOptions options = new CreateGarbageOptions(USAGE).parse(args);
+        String subCmd = options.getSubCmd();
+
+        LOG.info("Executing command: " + getClass().getName() + " with sub-command: " + subCmd);
+
+        if (CreateGarbageOptions.CMD_GENERATE.equals(subCmd)) {
+
+            if (GenerateGarbageHelper.isInvalidGarbageGenerationMode(options.getGarbageType())) {
+                LOG.error("Invalid fullGCMode specified: " + options.getGarbageType() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+            if (options.getCreateGarbageNodesCount() <= 0) {
+                LOG.error("Invalid garbageNodesCount specified: " + options.getCreateGarbageNodesCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+            if (options.getGarbageNodesParentCount() <= 0) {
+                LOG.error("Invalid garbageNodesParentCount specified: " + options.getGarbageNodesParentCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+
+            if (options.getNumberOfRuns() > 1 && options.getGenerateIntervalSeconds() > 0) {
+                generateBasePaths.addAll(generateGarbageContinuously(options, closer));
+            } else {
+                generateBasePaths.add(createGarbage(options, closer, 0));
+            }
+        } else if (CreateGarbageOptions.CMD_CLEAN.equals(subCmd)) {
+            cleanGarbage(options, closer);
+        } else {
+            System.err.println("Unknown revisions command: " + subCmd);
+        }
+
+        return generateBasePaths;
+    }
+
+    @Override
+    public void close() throws IOException {

Review Comment:
   We can safely remove this since we are registering `continuousRunExecutor` with a `closer` instance along with the `Closeable` interface.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "shodaaan (via GitHub)" <gi...@apache.org>.
shodaaan commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1832475302


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/CreateGarbageCommand.java:
##########
@@ -0,0 +1,574 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.base.Joiner;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.commons.concurrent.ExecutorCloser;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.GenerateGarbageHelper;
+import org.apache.jackrabbit.oak.plugins.document.LeaseCheckMode;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.asCloseable;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateGarbageCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class CreateGarbageCommand implements Command, Closeable {
+    private static final Logger LOG = LoggerFactory.getLogger(CreateGarbageCommand.class);
+
+    private final ScheduledExecutorService continuousRunExecutor = Executors.newScheduledThreadPool(1);
+
+    private static final String USAGE = Joiner.on(System.lineSeparator()).join(
+            "create-test-garbage {<jdbc-uri> | <mongodb-uri>} <sub-command> [options] ",
+            "where sub-command is one of: ",
+            "  clean - clean up all generated garbage under the specified root node",
+            "  generate - generate garbage nodes in the repository, under the root node tmp/oak-run-generated-test-garbage. ",
+            "           Use the --garbageType (required) option to specify the type of garbage to generate",
+            "           the --garbageNodesCount option to specify the total number of garbage nodes to create,",
+            "           the --garbageNodesParentCount option to specify the total number of parent nodes under which to create garbage nodes,",
+            "           the --orphansDepth option to specify the depth in the tree at which to create garbage nodes,",
+            "           the --orphansLevelGap option to specify the gap in the tree between the first leaf gap orphan garbage node and its parent,",
+            "           the --numberOfRuns option to specify the number of garbage generation runs to do,",
+            "           and the --generateIntervalSeconds option to specify the interval at which to generate a complete garbage count from createGarbageNotesCount.");
+
+
+    /**
+     * Garbage should be generated under tmp node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH_BASE = "tmp";
+
+    /**
+     * The root node name for garbage generation, one level under tmp.
+     */
+    public static String GARBAGE_GEN_ROOT_NODE_NAME = "oak-run-generated-test-garbage";
+
+    /**
+     * Root node for garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH = GARBAGE_GEN_ROOT_PATH_BASE + "/" + GARBAGE_GEN_ROOT_NODE_NAME;
+
+    /**
+     * Base path for garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String GEN_BASE_PATH = "GenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String GEN_PARENT_NODE_PREFIX = "Parent_";
+    public static String GEN_NODE_PREFIX = "Node_";
+    public static String GEN_NODE_LEVEL_PREFIX = "_Level_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    /**
+     * The maximum depth in the tree at which to generate gap orphans garbage nodes.
+     * Used for validation of the orphansDepth / orphansLevelGap options.
+     */
+    private final int GAP_ORPHANS_MAX_DEPTH = 15;
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class CreateGarbageOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";

Review Comment:
   Done, thank you for the suggestion.



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/CreateGarbageCommand.java:
##########
@@ -0,0 +1,574 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.base.Joiner;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.commons.concurrent.ExecutorCloser;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.GenerateGarbageHelper;
+import org.apache.jackrabbit.oak.plugins.document.LeaseCheckMode;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.asCloseable;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateGarbageCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class CreateGarbageCommand implements Command, Closeable {
+    private static final Logger LOG = LoggerFactory.getLogger(CreateGarbageCommand.class);
+
+    private final ScheduledExecutorService continuousRunExecutor = Executors.newScheduledThreadPool(1);
+
+    private static final String USAGE = Joiner.on(System.lineSeparator()).join(
+            "create-test-garbage {<jdbc-uri> | <mongodb-uri>} <sub-command> [options] ",
+            "where sub-command is one of: ",
+            "  clean - clean up all generated garbage under the specified root node",
+            "  generate - generate garbage nodes in the repository, under the root node tmp/oak-run-generated-test-garbage. ",
+            "           Use the --garbageType (required) option to specify the type of garbage to generate",
+            "           the --garbageNodesCount option to specify the total number of garbage nodes to create,",
+            "           the --garbageNodesParentCount option to specify the total number of parent nodes under which to create garbage nodes,",
+            "           the --orphansDepth option to specify the depth in the tree at which to create garbage nodes,",
+            "           the --orphansLevelGap option to specify the gap in the tree between the first leaf gap orphan garbage node and its parent,",
+            "           the --numberOfRuns option to specify the number of garbage generation runs to do,",
+            "           and the --generateIntervalSeconds option to specify the interval at which to generate a complete garbage count from createGarbageNotesCount.");
+
+
+    /**
+     * Garbage should be generated under tmp node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH_BASE = "tmp";
+
+    /**
+     * The root node name for garbage generation, one level under tmp.
+     */
+    public static String GARBAGE_GEN_ROOT_NODE_NAME = "oak-run-generated-test-garbage";
+
+    /**
+     * Root node for garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH = GARBAGE_GEN_ROOT_PATH_BASE + "/" + GARBAGE_GEN_ROOT_NODE_NAME;
+
+    /**
+     * Base path for garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String GEN_BASE_PATH = "GenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String GEN_PARENT_NODE_PREFIX = "Parent_";
+    public static String GEN_NODE_PREFIX = "Node_";
+    public static String GEN_NODE_LEVEL_PREFIX = "_Level_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    /**
+     * The maximum depth in the tree at which to generate gap orphans garbage nodes.
+     * Used for validation of the orphansDepth / orphansLevelGap options.
+     */
+    private final int GAP_ORPHANS_MAX_DEPTH = 15;
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class CreateGarbageOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node GARBAGE_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> orphansDepth;
+        final OptionSpec<Integer> orphansLevelGap;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public CreateGarbageOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            orphansDepth = parser
+                    .accepts("orphansDepth", "the depth in the tree at which to create garbage nodes. Only applies to ORPHANS fullGC modes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            orphansLevelGap = parser
+                    .accepts("orphansLevelGap", "the gap in the tree between the first leaf gap orphan garbage node and its' parent. Only applies to ORPHANS fullGC modes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public CreateGarbageOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getOrphansDepth() { return orphansDepth.value(options);}
+
+        public int getOrphansLevelGap() { return orphansLevelGap.value(options); }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {

Review Comment:
   Done, thank you for the suggestion.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "shodaaan (via GitHub)" <gi...@apache.org>.
shodaaan commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1809011333


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateFullGCCommand.java:
##########
@@ -0,0 +1,416 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.plugins.document.Collection;
+import org.apache.jackrabbit.oak.plugins.document.Document;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.DocumentStoreException;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateFullGCCommand implements Command {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateFullGCCommand.class);
+
+    private static final String USAGE = "generateFullGC {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String FULLGC_GEN_ROOT_PATH = "fullGCGenRoot";
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String FULLGC_GEN_BASE_PATH = "fullGCGenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String FULLGC_GEN_PARENT_NODE_PREFIX = "fullGCParent_";
+    public static String FULLGC_GEN_NODE_PREFIX = "fullGCNode_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node FULLGC_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public GenerateFullGCOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public GenerateFullGCOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {
+        Closer closer = Closer.create();
+        try {
+            execute(closer, args);
+        } catch (Throwable e) {
+            LOG.error("Command failed", e);
+            throw closer.rethrow(e);
+        } finally {
+            closer.close();
+        }
+    }
+
+    /**
+     * Method with passed closer is necessary in order to allow for unit tests to check the output of the command.
+     * It is the responsibility of the caller to close the closer.
+     *
+     * Returns the list of generated garbage base paths (under the garbage root node).
+     * @param closer
+     * @param args
+     * @throws Exception
+     */
+    public List<String> execute(Closer closer, String... args) throws Exception {
+        continuousRunIndex = 0;
+
+        List<String> generateBasePaths = new ArrayList<>();
+
+        GenerateFullGCOptions options = new GenerateFullGCOptions(USAGE).parse(args);
+        String subCmd = options.getSubCmd();
+
+        if (GenerateFullGCOptions.CMD_GENERATE.equals(subCmd)) {
+            if (options.getNumberOfRuns() > 1 && options.getGenerateIntervalSeconds() > 0) {
+                generateBasePaths.addAll(generateGarbageContinuously(options, closer));
+            } else {
+                generateBasePaths.add(generateGarbage(options, closer, 0));
+            }
+        } else if (GenerateFullGCOptions.CMD_CLEAN.equals(subCmd)) {
+            cleanGarbage(options, closer);
+        } else {
+            System.err.println("unknown revisions command: " + subCmd);
+        }
+
+        return generateBasePaths;
+    }
+
+    private List<String> generateGarbageContinuously(GenerateFullGCOptions options, Closer closer) throws IOException, Exception {
+        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long startGenTimestamp = System.currentTimeMillis();
+
+        List<String> generatedGarbageBasePaths = new ArrayList<>();
+
+        int numberOfRuns = options.getNumberOfRuns();
+        int intervalSeconds = options.getGenerateIntervalSeconds();
+        Runnable task = () -> {
+            if (continuousRunIndex < numberOfRuns) {
+                try {
+                    String genBasePath = generateGarbage(options, closer, continuousRunIndex, builder, startGenTimestamp);
+                    generatedGarbageBasePaths.add(genBasePath);
+                } catch (Exception e) {
+                    LOG.error("Error generating garbage in run " + continuousRunIndex, e);
+                }
+                LOG.info("Task executed. Count: " + (continuousRunIndex + 1));
+                continuousRunIndex++;
+            } else {
+                // Shutdown the executor once the task has run numberOfRuns times
+                executor.shutdown();
+                LOG.info("Task completed " + numberOfRuns + " times. Stopping execution.");
+            }
+        };
+
+        // Schedule the task to run every intervalSeconds
+        executor.scheduleAtFixedRate(task, 0, intervalSeconds, TimeUnit.SECONDS);
+
+        return generatedGarbageBasePaths;
+    }
+
+    /**
+     * Generate garbage nodes in the repository in order to allow for testing fullGC functionality.
+     *
+     * Returns the path of the generated FULLGC_GEN_BASE_PATH node (under the root).
+     * @param options
+     * @param closer
+     * @param runIndex
+     * @return
+     * @throws IOException
+     * @throws Exception
+     */
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long generationTimestamp = System.currentTimeMillis();
+
+        return generateGarbage(options, closer, runIndex, builder, generationTimestamp);
+    }
+
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex,
+                                  DocumentNodeStoreBuilder<?> builder, long timestamp) throws IOException, Exception {
+
+        if (builder == null) {
+            System.err.println("generateFullGC mode only available for DocumentNodeStore");
+            System.exit(1);
+        }
+
+        String generationBasePath = FULLGC_GEN_BASE_PATH + timestamp + "_" + runIndex;
+        System.out.println("Generating fullGC on the document: " + generationBasePath);
+        documentNodeStore = builder.build();
+
+        VersionGarbageCollector.FullGCMode fullGCMode = getFullGCMode(options);
+        if (fullGCMode == VersionGarbageCollector.FullGCMode.NONE) {
+            LOG.error("Invalid garbageType specified. Must be one of the following: 1 - EMPTYPROPS, 2 - GAP_ORPHANS, 3 - GAP_ORPHANS_EMPTYPROPS");
+            System.exit(1);
+        }
+
+        //1. Create nodes with properties
+        NodeBuilder rootNode = documentNodeStore.getRoot().builder();
+        NodeBuilder garbageRootNode = rootNode.child(FULLGC_GEN_ROOT_PATH);
+        garbageRootNode.child(generationBasePath).setProperty("jcr:primaryType", "nt:file", NAME);

Review Comment:
   Changed. Thank you for the suggestion.



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateFullGCCommand.java:
##########
@@ -0,0 +1,416 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.plugins.document.Collection;
+import org.apache.jackrabbit.oak.plugins.document.Document;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.DocumentStoreException;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateFullGCCommand implements Command {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateFullGCCommand.class);
+
+    private static final String USAGE = "generateFullGC {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String FULLGC_GEN_ROOT_PATH = "fullGCGenRoot";
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String FULLGC_GEN_BASE_PATH = "fullGCGenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String FULLGC_GEN_PARENT_NODE_PREFIX = "fullGCParent_";
+    public static String FULLGC_GEN_NODE_PREFIX = "fullGCNode_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node FULLGC_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public GenerateFullGCOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public GenerateFullGCOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {
+        Closer closer = Closer.create();
+        try {
+            execute(closer, args);
+        } catch (Throwable e) {
+            LOG.error("Command failed", e);
+            throw closer.rethrow(e);
+        } finally {
+            closer.close();
+        }
+    }
+
+    /**
+     * Method with passed closer is necessary in order to allow for unit tests to check the output of the command.
+     * It is the responsibility of the caller to close the closer.
+     *
+     * Returns the list of generated garbage base paths (under the garbage root node).
+     * @param closer
+     * @param args
+     * @throws Exception
+     */
+    public List<String> execute(Closer closer, String... args) throws Exception {
+        continuousRunIndex = 0;
+
+        List<String> generateBasePaths = new ArrayList<>();
+
+        GenerateFullGCOptions options = new GenerateFullGCOptions(USAGE).parse(args);
+        String subCmd = options.getSubCmd();
+
+        if (GenerateFullGCOptions.CMD_GENERATE.equals(subCmd)) {
+            if (options.getNumberOfRuns() > 1 && options.getGenerateIntervalSeconds() > 0) {
+                generateBasePaths.addAll(generateGarbageContinuously(options, closer));
+            } else {
+                generateBasePaths.add(generateGarbage(options, closer, 0));
+            }
+        } else if (GenerateFullGCOptions.CMD_CLEAN.equals(subCmd)) {
+            cleanGarbage(options, closer);
+        } else {
+            System.err.println("unknown revisions command: " + subCmd);
+        }
+
+        return generateBasePaths;
+    }
+
+    private List<String> generateGarbageContinuously(GenerateFullGCOptions options, Closer closer) throws IOException, Exception {
+        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long startGenTimestamp = System.currentTimeMillis();
+
+        List<String> generatedGarbageBasePaths = new ArrayList<>();
+
+        int numberOfRuns = options.getNumberOfRuns();
+        int intervalSeconds = options.getGenerateIntervalSeconds();
+        Runnable task = () -> {
+            if (continuousRunIndex < numberOfRuns) {
+                try {
+                    String genBasePath = generateGarbage(options, closer, continuousRunIndex, builder, startGenTimestamp);
+                    generatedGarbageBasePaths.add(genBasePath);
+                } catch (Exception e) {
+                    LOG.error("Error generating garbage in run " + continuousRunIndex, e);
+                }
+                LOG.info("Task executed. Count: " + (continuousRunIndex + 1));
+                continuousRunIndex++;
+            } else {
+                // Shutdown the executor once the task has run numberOfRuns times
+                executor.shutdown();
+                LOG.info("Task completed " + numberOfRuns + " times. Stopping execution.");
+            }
+        };
+
+        // Schedule the task to run every intervalSeconds
+        executor.scheduleAtFixedRate(task, 0, intervalSeconds, TimeUnit.SECONDS);
+
+        return generatedGarbageBasePaths;
+    }
+
+    /**
+     * Generate garbage nodes in the repository in order to allow for testing fullGC functionality.
+     *
+     * Returns the path of the generated FULLGC_GEN_BASE_PATH node (under the root).
+     * @param options
+     * @param closer
+     * @param runIndex
+     * @return
+     * @throws IOException
+     * @throws Exception
+     */
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long generationTimestamp = System.currentTimeMillis();
+
+        return generateGarbage(options, closer, runIndex, builder, generationTimestamp);
+    }
+
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex,
+                                  DocumentNodeStoreBuilder<?> builder, long timestamp) throws IOException, Exception {
+
+        if (builder == null) {
+            System.err.println("generateFullGC mode only available for DocumentNodeStore");
+            System.exit(1);
+        }
+
+        String generationBasePath = FULLGC_GEN_BASE_PATH + timestamp + "_" + runIndex;
+        System.out.println("Generating fullGC on the document: " + generationBasePath);
+        documentNodeStore = builder.build();
+
+        VersionGarbageCollector.FullGCMode fullGCMode = getFullGCMode(options);
+        if (fullGCMode == VersionGarbageCollector.FullGCMode.NONE) {
+            LOG.error("Invalid garbageType specified. Must be one of the following: 1 - EMPTYPROPS, 2 - GAP_ORPHANS, 3 - GAP_ORPHANS_EMPTYPROPS");

Review Comment:
   Changed. Thank you for the suggestion.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "shodaaan (via GitHub)" <gi...@apache.org>.
shodaaan commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1809008015


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/AvailableModes.java:
##########
@@ -75,5 +75,6 @@ public final class AvailableModes {
             .put("segment-copy", new SegmentCopyCommand())
             .put("server", new ServerCommand())
             .put("purge-index-versions", new LucenePurgeOldIndexVersionCommand())
+            .put("generateFullGC", new GenerateFullGCCommand())

Review Comment:
   Changed. Thank you for the suggestion.



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateFullGCCommand.java:
##########
@@ -0,0 +1,416 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.plugins.document.Collection;
+import org.apache.jackrabbit.oak.plugins.document.Document;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.DocumentStoreException;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateFullGCCommand implements Command {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateFullGCCommand.class);
+
+    private static final String USAGE = "generateFullGC {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String FULLGC_GEN_ROOT_PATH = "fullGCGenRoot";
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String FULLGC_GEN_BASE_PATH = "fullGCGenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String FULLGC_GEN_PARENT_NODE_PREFIX = "fullGCParent_";
+    public static String FULLGC_GEN_NODE_PREFIX = "fullGCNode_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node FULLGC_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public GenerateFullGCOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public GenerateFullGCOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {
+        Closer closer = Closer.create();
+        try {
+            execute(closer, args);
+        } catch (Throwable e) {
+            LOG.error("Command failed", e);
+            throw closer.rethrow(e);
+        } finally {
+            closer.close();
+        }
+    }
+
+    /**
+     * Method with passed closer is necessary in order to allow for unit tests to check the output of the command.
+     * It is the responsibility of the caller to close the closer.
+     *
+     * Returns the list of generated garbage base paths (under the garbage root node).
+     * @param closer
+     * @param args
+     * @throws Exception
+     */
+    public List<String> execute(Closer closer, String... args) throws Exception {
+        continuousRunIndex = 0;
+
+        List<String> generateBasePaths = new ArrayList<>();
+
+        GenerateFullGCOptions options = new GenerateFullGCOptions(USAGE).parse(args);
+        String subCmd = options.getSubCmd();
+
+        if (GenerateFullGCOptions.CMD_GENERATE.equals(subCmd)) {
+            if (options.getNumberOfRuns() > 1 && options.getGenerateIntervalSeconds() > 0) {
+                generateBasePaths.addAll(generateGarbageContinuously(options, closer));
+            } else {
+                generateBasePaths.add(generateGarbage(options, closer, 0));
+            }
+        } else if (GenerateFullGCOptions.CMD_CLEAN.equals(subCmd)) {
+            cleanGarbage(options, closer);
+        } else {
+            System.err.println("unknown revisions command: " + subCmd);
+        }
+
+        return generateBasePaths;
+    }
+
+    private List<String> generateGarbageContinuously(GenerateFullGCOptions options, Closer closer) throws IOException, Exception {
+        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);

Review Comment:
   Changed. Thank you for the suggestion.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "rishabhdaim (via GitHub)" <gi...@apache.org>.
rishabhdaim commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1808493200


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateFullGCCommand.java:
##########
@@ -0,0 +1,416 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.plugins.document.Collection;
+import org.apache.jackrabbit.oak.plugins.document.Document;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.DocumentStoreException;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateFullGCCommand implements Command {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateFullGCCommand.class);
+
+    private static final String USAGE = "generateFullGC {<jdbc-uri> | <mongodb-uri>} [options]";

Review Comment:
   ```suggestion
       private static final String USAGE = "createGarbage {<jdbc-uri> | <mongodb-uri>} [options]";
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "rishabhdaim (via GitHub)" <gi...@apache.org>.
rishabhdaim commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1820525382


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/AvailableModes.java:
##########
@@ -75,5 +75,6 @@ public final class AvailableModes {
             .put("segment-copy", new SegmentCopyCommand())
             .put("server", new ServerCommand())
             .put("purge-index-versions", new LucenePurgeOldIndexVersionCommand())
+            .put("generate-test-garbage", new GenerateGarbageCommand())

Review Comment:
   I would suggest changing `generate` to `create`.
   Generate is used with non-tangible things whereas `create` is used with tangible ones.
   
   `create` seems more accurate grammatically.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "stefan-egli (via GitHub)" <gi...@apache.org>.
stefan-egli merged PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "shodaaan (via GitHub)" <gi...@apache.org>.
shodaaan commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1809009332


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateFullGCCommand.java:
##########
@@ -0,0 +1,416 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.plugins.document.Collection;
+import org.apache.jackrabbit.oak.plugins.document.Document;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.DocumentStoreException;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateFullGCCommand implements Command {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateFullGCCommand.class);
+
+    private static final String USAGE = "generateFullGC {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String FULLGC_GEN_ROOT_PATH = "fullGCGenRoot";
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String FULLGC_GEN_BASE_PATH = "fullGCGenTest_";

Review Comment:
   Changed node names. Thank you for the suggestion.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "Joscorbe (via GitHub)" <gi...@apache.org>.
Joscorbe commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1813522487


##########
oak-run/src/test/java/org/apache/jackrabbit/oak/plugins/document/GenerateFullGCCommandTest.java:
##########
@@ -0,0 +1,293 @@
+/*
+ * 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 org.apache.jackrabbit.guava.common.collect.ImmutableList;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.plugins.document.util.MongoConnection;
+import org.apache.jackrabbit.oak.run.GenerateFullGCCommand;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.apache.jackrabbit.oak.plugins.document.CommandTestUtils.captureSystemOut;
+import static org.apache.jackrabbit.oak.run.GenerateFullGCCommand.EMPTY_PROPERTY_NAME;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+public class GenerateFullGCCommandTest {
+
+    final String OPTION_GARBAGE_NODES_COUNT = "--garbageNodesCount";
+    final String OPTION_GARBAGE_NODES_PARENT_COUNT = "--garbageNodesParentCount";
+    final String OPTION_GARBAGE_TYPE = "--garbageType";
+    final String OPTION_NUMBER_OF_RUNS = "--numberOfRuns";
+    final String OPTION_GENERATE_INTERVAL_SECONDS = "--generateIntervalSeconds";
+
+    @Rule
+    public MongoConnectionFactory connectionFactory = new MongoConnectionFactory();
+
+    @Rule
+    public DocumentMKBuilderProvider builderProvider = new DocumentMKBuilderProvider();
+
+    private DocumentNodeStore ns;
+
+    private Closer closer;
+
+    @BeforeClass
+    public static void assumeMongoDB() {
+        assumeTrue(MongoUtils.isAvailable());
+    }
+
+    @Before
+    public void before() {
+        ns = createDocumentNodeStore();
+    }
+
+    @After
+    public void after() {
+        try {
+            closer.close();
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+        ns.dispose();
+    }
+
+    private DocumentNodeStore createDocumentNodeStore() {
+        MongoConnection c = connectionFactory.getConnection();
+        assertNotNull(c);
+        MongoUtils.dropCollections(c.getDatabase());
+        return builderProvider.newBuilder().setFullGCEnabled(false).setLeaseCheckMode(LeaseCheckMode.DISABLED).setAsyncDelay(0)
+                .setMongoDB(c.getMongoClient(), c.getDBName()).getNodeStore();
+    }
+
+    private List<String> getChildNodeNames(NodeState node) {
+        List<String> childNodeNames = new ArrayList<>();
+        for (String childNodeName : node.getChildNodeNames()) {
+            childNodeNames.add(childNodeName);
+        }
+        return childNodeNames;
+    }
+
+    @Ignore
+    @Test
+    public void generateGarbageEmptyPropsUnderOneParentOnCustomMongoDB() {
+        ns.dispose();
+
+        // this should be a valid mongoDB connection string for the MongoDB on which to generate the garbage
+        String mongoDBConnString = "";
+        GenerateFullGCCmd cmd = new GenerateFullGCCmd( mongoDBConnString, OPTION_GARBAGE_NODES_COUNT, "10", OPTION_GARBAGE_NODES_PARENT_COUNT, "1",
+                OPTION_GARBAGE_TYPE, "1");
+        String output = captureSystemOut(cmd);
+
+        //sleep thread for 10 seconds to allow the garbage generation to complete
+        try {
+            Thread.sleep(10000);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+
+        DocumentNodeStore nodeStore = cmd.getCommand().getDocumentNodeStore();
+        NodeState garbageRootNodeState = nodeStore.getRoot().getChildNode(GenerateFullGCCommand.FULLGC_GEN_ROOT_PATH);
+        List<String> garbageRootNodeChildNames = getChildNodeNames(garbageRootNodeState);
+
+        assertEquals(garbageRootNodeChildNames.size(), 1);
+        List<String> propParentsGarbageNodeNames = getChildNodeNames(garbageRootNodeState.getChildNode(garbageRootNodeChildNames.get(0)));
+        assertEquals(propParentsGarbageNodeNames.size(), 1);
+
+        List<String> propGarbageNodeNames = getChildNodeNames(garbageRootNodeState.getChildNode(garbageRootNodeChildNames.get(0)).getChildNode(propParentsGarbageNodeNames.get(0)));
+        assertEquals(propGarbageNodeNames.size(), 10);
+    }
+
+    @Test
+    public void cleanupAllGarbage() {
+
+        // generate garbage first
+        GenerateFullGCCmd cmdGenerateGarbage = new GenerateFullGCCmd(OPTION_GARBAGE_NODES_COUNT, "10", OPTION_GARBAGE_NODES_PARENT_COUNT, "1",
+                OPTION_GARBAGE_TYPE, "2");
+        cmdGenerateGarbage.run();
+        Closer cmd1Closer = cmdGenerateGarbage.getCloser();
+
+        // cleanup garbage
+        String mongoDBConnString = "mongodb://cm-p107053-e251100-username:3fUEP9UHPD9yP4H@cmp107053e251100r355e01-shard-00-00.wxmqq.mongodb.net:27017,cmp107053e251100r355e01-shard-00-01.wxmqq.mongodb.net:27017,cmp107053e251100r355e01-shard-00-02.wxmqq.mongodb.net:27017/cm-p107053-e251100-database?ssl=true&authSource=admin&replicaSet=atlas-4x602o-shard-0&retryWrites=true&readPreference=primaryPreferred&readConcernLevel=majority&w=majority";

Review Comment:
   Check this please!



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "shodaaan (via GitHub)" <gi...@apache.org>.
shodaaan commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1838420201


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/CreateGarbageCommand.java:
##########
@@ -0,0 +1,568 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.base.Joiner;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.CreateGarbageHelper;
+import org.apache.jackrabbit.oak.plugins.document.LeaseCheckMode;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.asCloseable;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * CreateGarbageCommand creates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class CreateGarbageCommand implements Command {
+    private static final Logger LOG = LoggerFactory.getLogger(CreateGarbageCommand.class);
+
+    private final ScheduledExecutorService continuousRunExecutor = Executors.newScheduledThreadPool(1);
+
+    private static final String USAGE = Joiner.on(System.lineSeparator()).join(
+            "create-test-garbage {<jdbc-uri> | <mongodb-uri>} <sub-command> [options] ",
+            "where sub-command is one of: ",
+            "  clean - clean up all generated garbage under the specified root node",
+            "  create - create garbage nodes in the repository, under the root node tmp/oak-run-generated-test-garbage. ",
+            "           Use the --garbageType (required) option to specify the type of garbage to create",
+            "           the --garbageNodesCount option to specify the total number of garbage nodes to create,",
+            "           the --garbageNodesParentCount option to specify the total number of parent nodes under which to create garbage nodes,",
+            "           the --orphansDepth option to specify the depth in the tree at which to create garbage nodes,",
+            "           the --orphansLevelGap option to specify the gap in the tree between the first leaf gap orphan garbage node and its parent,",
+            "           the --numberOfRuns option to specify the number of garbage generation runs to do,",
+            "           and the --generateIntervalSeconds option to specify the interval at which to generate a complete garbage count from createGarbageNotesCount.");
+
+
+    /**
+     * Garbage should be generated under tmp node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH_BASE = "tmp";
+
+    /**
+     * The root node name for garbage generation, one level under tmp.
+     */
+    public static String GARBAGE_GEN_ROOT_NODE_NAME = "oak-run-generated-test-garbage";
+
+    /**
+     * Root node for garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH = GARBAGE_GEN_ROOT_PATH_BASE + "/" + GARBAGE_GEN_ROOT_NODE_NAME;
+
+    /**
+     * Base path for garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String GEN_BASE_PATH = "GenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String GEN_PARENT_NODE_PREFIX = "Parent_";
+    public static String GEN_NODE_PREFIX = "Node_";
+    public static String GEN_NODE_LEVEL_PREFIX = "_Level_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    /**
+     * The maximum depth in the tree at which to generate gap orphans garbage nodes.
+     * Used for validation of the orphansDepth / orphansLevelGap options.
+     */
+    private final int GAP_ORPHANS_MAX_DEPTH = 15;
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class CreateGarbageOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_CREATE = "create";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node GARBAGE_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> orphansDepth;
+        final OptionSpec<Integer> orphansLevelGap;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public CreateGarbageOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            orphansDepth = parser
+                    .accepts("orphansDepth", "the depth in the tree at which to create garbage nodes. Only applies to ORPHANS fullGC modes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            orphansLevelGap = parser
+                    .accepts("orphansLevelGap", "the gap in the tree between the first leaf gap orphan garbage node and its' parent. Only applies to ORPHANS fullGC modes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public CreateGarbageOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_CREATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getOrphansDepth() { return orphansDepth.value(options);}
+
+        public int getOrphansLevelGap() { return orphansLevelGap.value(options); }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    @Override
+    public void execute(String... args) throws Exception {
+
+        try (Closer closer = Closer.create()) {
+            // Register the ExecutorService with Closer
+            closer.register(() -> {
+                // Properly shutdown the executor service
+                continuousRunExecutor.shutdown();
+                try {
+                    if (!continuousRunExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
+                        continuousRunExecutor.shutdownNow();
+                    }
+                } catch (InterruptedException e) {
+                    throw new RuntimeException(e);
+                }
+            });
+
+            execute(closer, args);
+

Review Comment:
   Fixed, thank you for the comment.



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/CreateGarbageCommand.java:
##########
@@ -0,0 +1,568 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.base.Joiner;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.CreateGarbageHelper;
+import org.apache.jackrabbit.oak.plugins.document.LeaseCheckMode;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.asCloseable;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * CreateGarbageCommand creates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class CreateGarbageCommand implements Command {
+    private static final Logger LOG = LoggerFactory.getLogger(CreateGarbageCommand.class);
+
+    private final ScheduledExecutorService continuousRunExecutor = Executors.newScheduledThreadPool(1);
+
+    private static final String USAGE = Joiner.on(System.lineSeparator()).join(
+            "create-test-garbage {<jdbc-uri> | <mongodb-uri>} <sub-command> [options] ",
+            "where sub-command is one of: ",
+            "  clean - clean up all generated garbage under the specified root node",
+            "  create - create garbage nodes in the repository, under the root node tmp/oak-run-generated-test-garbage. ",
+            "           Use the --garbageType (required) option to specify the type of garbage to create",
+            "           the --garbageNodesCount option to specify the total number of garbage nodes to create,",
+            "           the --garbageNodesParentCount option to specify the total number of parent nodes under which to create garbage nodes,",
+            "           the --orphansDepth option to specify the depth in the tree at which to create garbage nodes,",
+            "           the --orphansLevelGap option to specify the gap in the tree between the first leaf gap orphan garbage node and its parent,",
+            "           the --numberOfRuns option to specify the number of garbage generation runs to do,",
+            "           and the --generateIntervalSeconds option to specify the interval at which to generate a complete garbage count from createGarbageNotesCount.");
+
+
+    /**
+     * Garbage should be generated under tmp node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH_BASE = "tmp";
+
+    /**
+     * The root node name for garbage generation, one level under tmp.
+     */
+    public static String GARBAGE_GEN_ROOT_NODE_NAME = "oak-run-generated-test-garbage";
+
+    /**
+     * Root node for garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH = GARBAGE_GEN_ROOT_PATH_BASE + "/" + GARBAGE_GEN_ROOT_NODE_NAME;
+
+    /**
+     * Base path for garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String GEN_BASE_PATH = "GenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String GEN_PARENT_NODE_PREFIX = "Parent_";
+    public static String GEN_NODE_PREFIX = "Node_";
+    public static String GEN_NODE_LEVEL_PREFIX = "_Level_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    /**
+     * The maximum depth in the tree at which to generate gap orphans garbage nodes.
+     * Used for validation of the orphansDepth / orphansLevelGap options.
+     */
+    private final int GAP_ORPHANS_MAX_DEPTH = 15;
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class CreateGarbageOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_CREATE = "create";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node GARBAGE_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> orphansDepth;
+        final OptionSpec<Integer> orphansLevelGap;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public CreateGarbageOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            orphansDepth = parser
+                    .accepts("orphansDepth", "the depth in the tree at which to create garbage nodes. Only applies to ORPHANS fullGC modes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            orphansLevelGap = parser
+                    .accepts("orphansLevelGap", "the gap in the tree between the first leaf gap orphan garbage node and its' parent. Only applies to ORPHANS fullGC modes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public CreateGarbageOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_CREATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getOrphansDepth() { return orphansDepth.value(options);}
+
+        public int getOrphansLevelGap() { return orphansLevelGap.value(options); }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    @Override
+    public void execute(String... args) throws Exception {
+
+        try (Closer closer = Closer.create()) {
+            // Register the ExecutorService with Closer
+            closer.register(() -> {
+                // Properly shutdown the executor service
+                continuousRunExecutor.shutdown();
+                try {
+                    if (!continuousRunExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
+                        continuousRunExecutor.shutdownNow();
+                    }
+                } catch (InterruptedException e) {
+                    throw new RuntimeException(e);
+                }
+            });
+
+            execute(closer, args);
+
+        } catch (Throwable e) {
+            LOG.error("Command failed", e);
+            throw e;
+        }
+    }
+
+    /**
+     * Method with passed closer is necessary in order to allow for unit tests to check the output of the command.
+     * It is the responsibility of the caller to close the closer.
+     *
+     * Returns the list of generated garbage base paths (under the garbage root node).
+     * @param closer
+     * @param args
+     * @throws Exception
+     */
+    public List<String> execute(Closer closer, String... args) throws Exception {
+        continuousRunIndex = 0;
+
+        List<String> generateBasePaths = new ArrayList<>();
+
+        CreateGarbageOptions options = new CreateGarbageOptions(USAGE).parse(args);
+        String subCmd = options.getSubCmd();
+
+        LOG.info("Executing command: " + getClass().getName() + " with sub-command: " + subCmd);
+
+        if (CreateGarbageOptions.CMD_CREATE.equals(subCmd)) {
+
+            if (CreateGarbageHelper.isInvalidGarbageGenerationMode(options.getGarbageType())) {
+                LOG.error("Invalid fullGCMode specified: " + options.getGarbageType() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+            if (options.getCreateGarbageNodesCount() <= 0) {
+                LOG.error("Invalid garbageNodesCount specified: " + options.getCreateGarbageNodesCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+            if (options.getGarbageNodesParentCount() <= 0) {
+                LOG.error("Invalid garbageNodesParentCount specified: " + options.getGarbageNodesParentCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+
+            if (options.getNumberOfRuns() > 1 && options.getGenerateIntervalSeconds() > 0) {
+                generateBasePaths.addAll(generateGarbageContinuously(options, closer));
+            } else {
+                generateBasePaths.add(createGarbage(options, closer, 0));
+            }
+        } else if (CreateGarbageOptions.CMD_CLEAN.equals(subCmd)) {
+            cleanGarbage(options, closer);
+        } else {
+            System.err.println("Unknown revisions command: " + subCmd);
+        }
+
+        return generateBasePaths;
+    }
+
+    private List<String> generateGarbageContinuously(CreateGarbageOptions options, Closer closer) throws Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long startGenTimestamp = System.currentTimeMillis();
+
+        List<String> generatedGarbageBasePaths = new ArrayList<>();
+
+        int numberOfRuns = options.getNumberOfRuns();
+        int intervalSeconds = options.getGenerateIntervalSeconds();
+        Runnable task = () -> {
+            if (continuousRunIndex < numberOfRuns) {
+                try {
+                    String genBasePath = createGarbage(options, closer, continuousRunIndex, builder, startGenTimestamp);
+                    generatedGarbageBasePaths.add(genBasePath);
+                } catch (Exception e) {
+                    LOG.error("Error generating garbage in run " + continuousRunIndex, e);
+                }
+                LOG.info("Task executed. Count: " + (continuousRunIndex + 1));
+                continuousRunIndex++;
+            } else {
+                // Shutdown the executor once the task has run numberOfRuns times
+                continuousRunExecutor.shutdown();
+                LOG.info("Task completed " + numberOfRuns + " times. Stopping execution.");
+            }
+        };
+
+        // Schedule the task to run every intervalSeconds
+        continuousRunExecutor.scheduleAtFixedRate(task, 0, intervalSeconds, TimeUnit.SECONDS);
+
+        return generatedGarbageBasePaths;
+    }
+
+    /**
+     * Generate garbage nodes in the repository in order to allow for testing fullGC functionality.
+     *
+     * Returns the path of the generated GARBAGE_GEN_ROOT_PATH node (under the root).
+     * @param options
+     * @param closer
+     * @param runIndex
+     * @return
+     * @throws IOException
+     * @throws Exception
+     */
+    private String createGarbage(CreateGarbageOptions options, Closer closer, int runIndex) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long generationTimestamp = System.currentTimeMillis();
+
+        return createGarbage(options, closer, runIndex, builder, generationTimestamp);
+    }
+
+    private String createGarbage(CreateGarbageOptions options, Closer closer, int runIndex,
+                                 DocumentNodeStoreBuilder<?> builder, long timestamp) throws Exception {
+
+        if (builder == null) {
+            System.err.println("generateGarbage mode only available for DocumentNodeStore");
+            System.exit(1);
+        }
+
+        String generationBasePath = GEN_BASE_PATH + timestamp + "_" + runIndex;
+        documentNodeStore = builder.build();
+        closer.register(asCloseable(documentNodeStore));
+
+        NodeBuilder rootNode = documentNodeStore.getRoot().builder();
+
+        System.out.println("Starting garbage generation under the root node tmp/oak-run-generated-test-garbage");
+
+        if (CreateGarbageHelper.isEmptyProps(options.getGarbageType())) {
+            createGarbageEmptyProps(rootNode, options, generationBasePath);
+        }
+        else if (CreateGarbageHelper.isGapOrphans(options.getGarbageType())) {
+            createGarbageGapOrphans(rootNode, options, generationBasePath);
+        }
+
+        return generationBasePath;
+    }
+    private void createGarbageGapOrphans(NodeBuilder rootNode, CreateGarbageOptions options, String generationBasePath)
+            throws CommitFailedException {
+
+        // validate gap orphans depth and level gap
+        if (options.getOrphansDepth() < 0 || options.getOrphansDepth() > GAP_ORPHANS_MAX_DEPTH) {
+            LOG.error("Invalid orphansDepth specified: " + options.getOrphansDepth() + " in: " + getClass().getName()
+                    + ". Must be an integer >= 0 and <= " + GAP_ORPHANS_MAX_DEPTH);
+            System.exit(1);
+        }
+        if (options.getOrphansLevelGap() < 0 || options.getOrphansLevelGap() > options.getOrphansDepth()) {
+            LOG.error("Invalid orphansLevelGap specified: " + options.getOrphansLevelGap() + " in: " + getClass().getName()
+                    + ". Must be a positive integer <= orphansDepth");
+            System.exit(1);
+        }
+
+        System.out.println("Generating gap orphans garbage on the document: " + generationBasePath);
+
+        NodeBuilder garbageRootNode = rootNode.child(GARBAGE_GEN_ROOT_PATH_BASE).child(GARBAGE_GEN_ROOT_NODE_NAME);
+        garbageRootNode.child(generationBasePath).setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+
+        int nodesCountUnderParent = options.getCreateGarbageNodesCount() / options.getGarbageNodesParentCount();
+        for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+            // create parent node
+            garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).setProperty(JcrConstants.JCR_PRIMARYTYPE, "nt:folder", NAME);
+
+            // create child nodes under parent, according to gap orphans depth
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                getGapOrphanLeafGarbageNode(garbageRootNode, generationBasePath, options.getOrphansDepth(), i, j).
+                        setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+            }
+        }
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+
+        // Create garbage nodes - GAP_ORPHANS - remove parent nodes
+        StringBuilder sbNodePath = new StringBuilder();
+        List<String> deleteNodePaths = new ArrayList<>();
+        for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+
+            // append the parent to the paths to delete
+            sbNodePath.setLength(0);
+            String path = getIdFromPath(
+                    sbNodePath.append("/").append(GARBAGE_GEN_ROOT_PATH).append("/").append(generationBasePath).append("/")
+                            .append(GEN_PARENT_NODE_PREFIX).append(i).toString());
+            deleteNodePaths.add(path);
+
+            // append all the gap orphans nodes between the parent and the level gap to the paths to delete
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                deleteNodePaths.addAll(generateGapOrphanNodePaths(generationBasePath, i, j, options.getOrphansDepth(), options.getOrphansLevelGap(), false));
+            }
+        }
+        // Remove all parent nodes
+        documentNodeStore.getDocumentStore().remove(org.apache.jackrabbit.oak.plugins.document.Collection.NODES, deleteNodePaths);
+    }
+
+    private NodeBuilder getGapOrphanLeafGarbageNode(NodeBuilder garbageRootNode, String generationBasePath, int depth, int parentIndex, int nodeIndex) {
+        NodeBuilder garbageNode = garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + parentIndex);
+        for (int i = 1; i < depth; i++) {
+            garbageNode = garbageNode.child(GEN_NODE_PREFIX + nodeIndex + GEN_NODE_LEVEL_PREFIX + i);
+        }
+        // the leaf garbage node does not have level in its name
+        garbageNode = garbageNode.child(GEN_NODE_PREFIX + nodeIndex);
+        return garbageNode;
+    }
+
+    /**
+     * Gets the paths of the nodes between the parent node and the level gap OR between the level gap and the leaf
+     * gap orphan node.
+     *
+     * Example 1: gapDepth = 4, gapLevelGap = 2, fromLevelGapToLeaf = false will return:
+     * - /tmp/oak-run-generated-test-garbage/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1
+     * - /tmp/oak-run-generated-test-garbage/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2
+     * and will NOT return the rest of the nodes:
+     * - /tmp/oak-run-generated-test-garbage/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3
+     * - /tmp/oak-run-generated-test-garbage/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3/Node_0 (the leaf node)
+     *
+     * Example 2: gapDepth = 5, gapLevelGap = 2, fromLevelGapToLeaf = true will return:
+     * - /tmp/oak-run-generated-test-garbage/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3
+     * - /tmp/oak-run-generated-test-garbage/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3/Node_0_Level_4
+     * - /tmp/oak-run-generated-test-garbage/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3/Node_0_Level_4/Node_0 (the leaf node)
+     * and will NOT return the nodes from the parent to the gap level:
+     * - /tmp/oak-run-generated-test-garbage/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1
+     * - /tmp/oak-run-generated-test-garbage/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2
+     *
+     * @param generationBasePath
+     * @param parentIndex
+     * @param nodeIndex
+     * @param gapDepth - the depth in the tree until which to create gap orphan garbage nodes.
+     * @param gapLevelGap - the gap in the tree between the first gap orphan garbage node and its parent.
+     * @param fromLevelGapToLeaf - if true, the paths will be generated from the level gap to the leaf node, otherwise the paths will be generated from the parent node to the level gap.
+     * @return
+     */
+    public static List<String> generateGapOrphanNodePaths(String generationBasePath, int parentIndex, int nodeIndex, int gapDepth, int gapLevelGap, boolean fromLevelGapToLeaf) {
+        List<String> gapOrphanNodePaths = new ArrayList<>();
+
+        StringBuilder sbNodePath = new StringBuilder();
+        sbNodePath.setLength(0);
+
+        // start path from the parent node
+        sbNodePath.append("/").append(GARBAGE_GEN_ROOT_PATH).append("/").append(generationBasePath).append("/")
+                .append(GEN_PARENT_NODE_PREFIX).append(parentIndex);
+
+        for(int i = 1; i <= gapDepth; i ++) {
+            sbNodePath.append("/").append(GEN_NODE_PREFIX).append(nodeIndex);
+            // the leaf node does NOT have level in its name
+            if (i < gapDepth) {
+                sbNodePath.append(GEN_NODE_LEVEL_PREFIX).append(i);
+            }
+
+            // only append to list if the criteria for returning the node path is met
+            if ((!fromLevelGapToLeaf && i < gapLevelGap) || (fromLevelGapToLeaf && i >= gapLevelGap)) {
+                gapOrphanNodePaths.add(getIdFromPath(sbNodePath.toString()));
+            }
+        }
+
+        return gapOrphanNodePaths;
+    }
+
+    private void createGarbageEmptyProps(NodeBuilder rootNode, CreateGarbageOptions options, String generationBasePath) throws CommitFailedException {
+
+        System.out.println("Creating empty props garbage on the document: " + generationBasePath);
+
+        NodeBuilder garbageRootNode = rootNode.child(GARBAGE_GEN_ROOT_PATH_BASE).child(GARBAGE_GEN_ROOT_NODE_NAME);
+        garbageRootNode.child(generationBasePath).setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+
+        int nodesCountUnderParent = options.getCreateGarbageNodesCount() / options.getGarbageNodesParentCount();
+        for(int i = 0; i < options.getGarbageNodesParentCount(); i ++) {
+            garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).setProperty(JcrConstants.JCR_PRIMARYTYPE, "nt:folder", NAME);
+
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                        setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+
+                if (CreateGarbageHelper.isEmptyProps(options.getGarbageType())) {
+                    garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                            setProperty(EMPTY_PROPERTY_NAME, "bar", STRING);
+                }
+            }
+        }
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+
+
+        //2. Create garbage nodes - EMPTY_PROPERTIES
+        for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+            for (int j = 0; j < nodesCountUnderParent; j++) {
+                garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                        removeProperty(EMPTY_PROPERTY_NAME);
+            }
+        }
+
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+    }
+
+    /**
+     * Cleans up all generated garbage by removing the node GARBAGE_GEN_ROOT_PATH and all of
+     * its children (recursively)
+     * @param options
+     * @param closer
+     * @throws IOException
+     * @throws Exception
+     */
+    private void cleanGarbage(CreateGarbageOptions options, Closer closer) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer).
+                setLeaseCheckMode(LeaseCheckMode.DISABLED).
+                setAsyncDelay(0).
+                setFullGCEnabled(false);
+
+        if (builder == null) {
+            System.err.println("generateGarbage mode only available for DocumentNodeStore");
+            System.exit(1);
+        }
+
+        System.out.println("Cleaning up all generated garbage: ");
+        documentNodeStore = builder.build();
+        closer.register(asCloseable(documentNodeStore));
+
+        NodeBuilder rootBuilder = documentNodeStore.getRoot().builder();
+
+        NodeBuilder generatedGarbageRootBuilder = rootBuilder.child(GARBAGE_GEN_ROOT_PATH_BASE).child(GARBAGE_GEN_ROOT_NODE_NAME);
+
+        String garbageRootNodePath = "/" + GARBAGE_GEN_ROOT_PATH;
+        List<String> childNodePaths = new ArrayList<>();
+        childNodePaths.add(getIdFromPath(garbageRootNodePath));
+
+        // get all paths of the tree nodes under the garbage root node
+        getTreeNodePaths(generatedGarbageRootBuilder, garbageRootNodePath, childNodePaths);
+
+        // check that only nodes under the garbage root node are being removed
+        for (String childNodePath : childNodePaths) {
+            assert childNodePath.substring(2).startsWith("/" + GARBAGE_GEN_ROOT_PATH);
+            System.out.println("Removing node: " + childNodePath);
+        }
+
+        documentNodeStore.getDocumentStore().remove(org.apache.jackrabbit.oak.plugins.document.Collection.NODES, childNodePaths);
+    }
+
+    /**
+     * Recursively get all paths of the tree nodes under the given root node.
+     * If there are any gaps in the tree, the paths of the gap nodes will NOT be included.
+     * @param rootNode
+     * @param basePath
+     * @param treeNodePaths
+     */
+    private void getTreeNodePaths(NodeBuilder rootNode, String basePath,
+                                  List<String> treeNodePaths) {

Review Comment:
   Fixed, thank you for the comment.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "shodaaan (via GitHub)" <gi...@apache.org>.
shodaaan commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1832474601


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/plugins/document/GenerateGarbageHelper.java:
##########
@@ -0,0 +1,37 @@
+/*
+ * 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;
+
+/**
+ * Helper class to generate garbage for testing purposes.
+ */
+public class GenerateGarbageHelper {

Review Comment:
   Done, thank you for the suggestion.



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/CreateGarbageCommand.java:
##########
@@ -0,0 +1,574 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.base.Joiner;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.commons.concurrent.ExecutorCloser;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.GenerateGarbageHelper;
+import org.apache.jackrabbit.oak.plugins.document.LeaseCheckMode;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.asCloseable;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateGarbageCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.

Review Comment:
   Done, thank you for the suggestion.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "shodaaan (via GitHub)" <gi...@apache.org>.
shodaaan commented on PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#issuecomment-2460170282

   > > > Regarding the explicit check, I don't think this is necessary
   > > 
   > > 
   > > Fair enough.
   > > Two unrelated additional comments:
   > > 
   > > * the DocumentNodeStore is not properly disposed. You can see this by running the [oak-run](https://issues.apache.org/jira/browse/OAK-run) command consecutively : the second start will have to wait 2min for the lease timeout of the first non-gracefully shut down run. We should ensure a graceful disposal.
   > > * what might be useful is to add the base path where the garbage is generated to - such that the user immediately knows (and doesn't eg have to consult the source code)
   > 
   > I added documentNodeStore.dispose() calls to CreateGarbageCommand. Thank you for the suggestion
   > 
   > The root path where garbage is generated (/tmp/[oak-run-generated-test-garbage](https://issues.apache.org/jira/browse/OAK-run-generated-test-garbage)) is specified in the USAGE string of CreateGarbageCommand, so users will know what the path is where garbage is generated.
   
   
   I added proper disposal of DocumentNodeStore for both createGarbage and cleanupGarbage commands. Tested oak-run with repeated runs and disposal works correctly.
   
   I also added some println messages with the base path of the generated garbage, as well as the paths of the documents deleted by the "clean" sub-command.
   
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "rishabhdaim (via GitHub)" <gi...@apache.org>.
rishabhdaim commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1804583984


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/AvailableModes.java:
##########
@@ -75,5 +75,6 @@ public final class AvailableModes {
             .put("segment-copy", new SegmentCopyCommand())
             .put("server", new ServerCommand())
             .put("purge-index-versions", new LucenePurgeOldIndexVersionCommand())
+            .put("generateFullGC", new GenerateFullGCCommand())

Review Comment:
   ```suggestion
               .put("createGarbage", new CreateGarbageCommand())
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "shodaaan (via GitHub)" <gi...@apache.org>.
shodaaan commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1809011042


##########
oak-run/src/test/java/org/apache/jackrabbit/oak/plugins/document/GenerateFullGCCommandTest.java:
##########
@@ -0,0 +1,293 @@
+/*
+ * 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 org.apache.jackrabbit.guava.common.collect.ImmutableList;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.plugins.document.util.MongoConnection;
+import org.apache.jackrabbit.oak.run.GenerateFullGCCommand;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.apache.jackrabbit.oak.plugins.document.CommandTestUtils.captureSystemOut;
+import static org.apache.jackrabbit.oak.run.GenerateFullGCCommand.EMPTY_PROPERTY_NAME;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+public class GenerateFullGCCommandTest {
+
+    final String OPTION_GARBAGE_NODES_COUNT = "--garbageNodesCount";
+    final String OPTION_GARBAGE_NODES_PARENT_COUNT = "--garbageNodesParentCount";
+    final String OPTION_GARBAGE_TYPE = "--garbageType";
+    final String OPTION_NUMBER_OF_RUNS = "--numberOfRuns";
+    final String OPTION_GENERATE_INTERVAL_SECONDS = "--generateIntervalSeconds";
+
+    @Rule
+    public MongoConnectionFactory connectionFactory = new MongoConnectionFactory();
+
+    @Rule
+    public DocumentMKBuilderProvider builderProvider = new DocumentMKBuilderProvider();
+
+    private DocumentNodeStore ns;
+
+    private Closer closer;
+
+    @BeforeClass
+    public static void assumeMongoDB() {
+        assumeTrue(MongoUtils.isAvailable());
+    }
+
+    @Before
+    public void before() {
+        ns = createDocumentNodeStore();
+    }
+
+    @After
+    public void after() {
+        try {
+            closer.close();
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+        ns.dispose();
+    }
+
+    private DocumentNodeStore createDocumentNodeStore() {
+        MongoConnection c = connectionFactory.getConnection();
+        assertNotNull(c);
+        MongoUtils.dropCollections(c.getDatabase());
+        return builderProvider.newBuilder().setFullGCEnabled(false).setLeaseCheckMode(LeaseCheckMode.DISABLED).setAsyncDelay(0)
+                .setMongoDB(c.getMongoClient(), c.getDBName()).getNodeStore();
+    }
+
+    private List<String> getChildNodeNames(NodeState node) {
+        List<String> childNodeNames = new ArrayList<>();
+        for (String childNodeName : node.getChildNodeNames()) {
+            childNodeNames.add(childNodeName);
+        }
+        return childNodeNames;
+    }
+
+    @Ignore
+    @Test
+    public void generateGarbageEmptyPropsUnderOneParentOnCustomMongoDB() {
+        ns.dispose();
+
+        // this should be a valid mongoDB connection string for the MongoDB on which to generate the garbage
+        String mongoDBConnString = "";
+        GenerateFullGCCmd cmd = new GenerateFullGCCmd( mongoDBConnString, OPTION_GARBAGE_NODES_COUNT, "10", OPTION_GARBAGE_NODES_PARENT_COUNT, "1",
+                OPTION_GARBAGE_TYPE, "1");
+        String output = captureSystemOut(cmd);
+
+        //sleep thread for 10 seconds to allow the garbage generation to complete
+        try {
+            Thread.sleep(10000);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+
+        DocumentNodeStore nodeStore = cmd.getCommand().getDocumentNodeStore();
+        NodeState garbageRootNodeState = nodeStore.getRoot().getChildNode(GenerateFullGCCommand.FULLGC_GEN_ROOT_PATH);
+        List<String> garbageRootNodeChildNames = getChildNodeNames(garbageRootNodeState);
+
+        assertEquals(garbageRootNodeChildNames.size(), 1);
+        List<String> propParentsGarbageNodeNames = getChildNodeNames(garbageRootNodeState.getChildNode(garbageRootNodeChildNames.get(0)));
+        assertEquals(propParentsGarbageNodeNames.size(), 1);
+
+        List<String> propGarbageNodeNames = getChildNodeNames(garbageRootNodeState.getChildNode(garbageRootNodeChildNames.get(0)).getChildNode(propParentsGarbageNodeNames.get(0)));
+        assertEquals(propGarbageNodeNames.size(), 10);
+    }
+
+    @Test
+    public void cleanupAllGarbage() {
+
+        // generate garbage first
+        GenerateFullGCCmd cmdGenerateGarbage = new GenerateFullGCCmd(OPTION_GARBAGE_NODES_COUNT, "10", OPTION_GARBAGE_NODES_PARENT_COUNT, "1",
+                OPTION_GARBAGE_TYPE, "2");
+        cmdGenerateGarbage.run();
+        Closer cmd1Closer = cmdGenerateGarbage.getCloser();
+
+        // cleanup garbage
+        String mongoDBConnString = "mongodb://cm-p107053-e251100-username:3fUEP9UHPD9yP4H@cmp107053e251100r355e01-shard-00-00.wxmqq.mongodb.net:27017,cmp107053e251100r355e01-shard-00-01.wxmqq.mongodb.net:27017,cmp107053e251100r355e01-shard-00-02.wxmqq.mongodb.net:27017/cm-p107053-e251100-database?ssl=true&authSource=admin&replicaSet=atlas-4x602o-shard-0&retryWrites=true&readPreference=primaryPreferred&readConcernLevel=majority&w=majority";
+        GenerateFullGCCmd cmdClean = new GenerateFullGCCmd(mongoDBConnString, "clean");
+        cmdClean.run();
+        closer = cmdClean.getCloser();
+
+        NodeDocument garbageRoot = cmdClean.getCommand().getDocument(Collection.NODES, "1:/" + GenerateFullGCCommand.FULLGC_GEN_ROOT_PATH, 0);
+        // garbage root node should be either deleted or empty (no children)
+        assertTrue(garbageRoot == null || !garbageRoot.hasChildren());
+
+        try {
+            cmd1Closer.close();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Test
+    public void generateGarbageEmptyPropsUnderOneParent() {
+        ns.dispose();
+
+        GenerateFullGCCmd cmd = new GenerateFullGCCmd(OPTION_GARBAGE_NODES_COUNT, "10", OPTION_GARBAGE_NODES_PARENT_COUNT, "1",
+                OPTION_GARBAGE_TYPE, "1");
+        String output = captureSystemOut(cmd);
+        closer = cmd.getCloser();
+
+        List<String> generatedBasePaths = cmd.getGeneratedBasePaths();
+
+        DocumentNodeStore nodeStore = cmd.getCommand().getDocumentNodeStore();
+        NodeState garbageRootNodeState = nodeStore.getRoot().getChildNode(GenerateFullGCCommand.FULLGC_GEN_ROOT_PATH);
+        List<String> garbageRootNodeChildNames = getChildNodeNames(garbageRootNodeState);
+
+        assertEquals(garbageRootNodeChildNames.size(), 1);
+        for (String generateBasePath : generatedBasePaths) {
+            assertTrue(garbageRootNodeChildNames.contains(generateBasePath));
+        }
+
+        List<String> propParentsGarbageNodeNames = getChildNodeNames(garbageRootNodeState.getChildNode(garbageRootNodeChildNames.get(0)));
+        assertEquals(propParentsGarbageNodeNames.size(), 1);
+
+        NodeState emptyPropsParentNode = garbageRootNodeState.getChildNode(garbageRootNodeChildNames.get(0)).getChildNode(propParentsGarbageNodeNames.get(0));
+        List<String> propGarbageNodeNames = getChildNodeNames(emptyPropsParentNode);
+        assertEquals(propGarbageNodeNames.size(), 10);
+
+        for (String propGarbageNodeName : propGarbageNodeNames) {
+            NodeState propGarbageNode = emptyPropsParentNode.getChildNode(propGarbageNodeName);
+            assertNull(propGarbageNode.getProperty(EMPTY_PROPERTY_NAME));
+        }
+    }
+
+    @Test
+    public void generateGarbageGapOrphansUnderOneParent() {
+        ns.dispose();
+
+        int garbageNodesCount = 10;
+        int garbageNodesParentCount = 1;
+
+        testGenerateGapOrphans(garbageNodesCount, garbageNodesParentCount);
+    }
+
+    @Test
+    public void generateGarbageGapOrphansUnderMultipleParents() {
+        ns.dispose();
+
+        int garbageNodesCount = 20;
+        int garbageNodesParentCount = 4;
+
+        testGenerateGapOrphans(garbageNodesCount, garbageNodesParentCount);
+    }
+
+    /**
+     * Calls the GenerateFullGCCommand to generate gap orphans and verifies the generated gap orphans:
+     * - checks that parents of gap orphans are missing by retrieving the nodes from the document store
+     * - checks that the expected number gap orphans are generated by retrieving the nodes from the document store
+     * @param garbageNodesCount
+     * @param garbageNodesParentCount
+     */
+    private void testGenerateGapOrphans(int garbageNodesCount, int garbageNodesParentCount) {
+        GenerateFullGCCmd cmd = new GenerateFullGCCmd(OPTION_GARBAGE_NODES_COUNT, String.valueOf(garbageNodesCount),
+                OPTION_GARBAGE_NODES_PARENT_COUNT, String.valueOf(garbageNodesParentCount),
+                OPTION_GARBAGE_TYPE, "2");
+        String output = captureSystemOut(cmd);
+        closer = cmd.getCloser();
+
+        int nodesPerParent = garbageNodesCount / garbageNodesParentCount;
+
+        List<String> generatedBasePaths = cmd.getGeneratedBasePaths();
+
+        DocumentNodeStore nodeStore = cmd.getCommand().getDocumentNodeStore();
+        NodeState garbageRootNodeState = nodeStore.getRoot().getChildNode(GenerateFullGCCommand.FULLGC_GEN_ROOT_PATH);
+        List<String> garbageRootNodeChildNames = getChildNodeNames(garbageRootNodeState);
+
+        assertEquals(garbageRootNodeChildNames.size(), 1);
+        for (String generateBasePath : generatedBasePaths) {
+            assertTrue(garbageRootNodeChildNames.contains(generateBasePath));
+        }
+
+        String generateBasePath = generatedBasePaths.get(0);
+        for (int i = 0; i < garbageNodesParentCount; i ++) {
+            NodeDocument missingDocument = cmd.getCommand().getDocument(Collection.NODES, "3:/"

Review Comment:
   Changed. Thank you for the suggestion.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "shodaaan (via GitHub)" <gi...@apache.org>.
shodaaan commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1809016889


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateFullGCCommand.java:
##########
@@ -0,0 +1,416 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.plugins.document.Collection;
+import org.apache.jackrabbit.oak.plugins.document.Document;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.DocumentStoreException;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateFullGCCommand implements Command {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateFullGCCommand.class);
+
+    private static final String USAGE = "generateFullGC {<jdbc-uri> | <mongodb-uri>} [options]";

Review Comment:
   Done, thank you for the suggestion.



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateFullGCCommand.java:
##########
@@ -0,0 +1,416 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.plugins.document.Collection;
+import org.apache.jackrabbit.oak.plugins.document.Document;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.DocumentStoreException;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateFullGCCommand implements Command {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateFullGCCommand.class);
+
+    private static final String USAGE = "generateFullGC {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String FULLGC_GEN_ROOT_PATH = "fullGCGenRoot";
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String FULLGC_GEN_BASE_PATH = "fullGCGenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String FULLGC_GEN_PARENT_NODE_PREFIX = "fullGCParent_";
+    public static String FULLGC_GEN_NODE_PREFIX = "fullGCNode_";

Review Comment:
   Done, thank you for the suggestion.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "shodaaan (via GitHub)" <gi...@apache.org>.
shodaaan commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1812639251


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/plugins/document/GenerateGarbageHelper.java:
##########
@@ -0,0 +1,19 @@
+package org.apache.jackrabbit.oak.plugins.document;
+
+/**
+ * Helper class to generate garbage for testing purposes.
+ */
+public class GenerateGarbageHelper {
+
+    public static boolean isInvalidGarbageGenerationMode(int fullGCMode) {
+        return fullGCMode == 0;
+    }
+
+    public static boolean includesEmptyProps(int fullGCMode) {
+        return fullGCMode == 1;
+    }
+
+    public static boolean includesGapOrphans(int fullGCMode) {

Review Comment:
   Fixed, thank you for the suggestion.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "shodaaan (via GitHub)" <gi...@apache.org>.
shodaaan commented on PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#issuecomment-2443503702

   > Added a few more points.
   > 
   > One general question is : as this command is clearly a test command not to be used against a production environment : how could that be emphasized. For example: we could add an explicit: "are you really sure you want to generate test garbage against ...". That would make it clear that the target repository is going to be written to. Wdyt?
   
   I resolved all of the points, thank you for the suggestions.
   
   Regarding the explicit check, I don't think this is necessary because oak-run is a tool that by design makes important / deep changes in the MongoDB it is run on, so users should always be careful and know the exact consequences of the commands they run. 
   The USAGE text clearly states what the command and associated subcommands do.
   Also, since oak-run commands are designed to be used "with intent", and not "by accident", having an additional confirmation step for each garbage generation command run would just annoy the user IMHO. I didn't see this check for any other command in oak-run.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "rishabhdaim (via GitHub)" <gi...@apache.org>.
rishabhdaim commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1809971053


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateGarbageCommand.java:
##########
@@ -0,0 +1,427 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.commons.concurrent.ExecutorCloser;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.GenerateGarbageHelper;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateGarbageCommand implements Command, Closeable {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateGarbageCommand.class);
+
+    private final ScheduledExecutorService continuousRunExecutor = Executors.newScheduledThreadPool(1);
+
+    private static final String USAGE = "createGarbage {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * FullGC garbage should be generated under tmp node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH_BASE = "tmp";
+
+    /**
+     * The root node name for fullGC garbage generation, one level under tmp.
+     */
+    public static String GARBAGE_GEN_ROOT_NODE_NAME = "fullGC_GarbageRoot";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH = GARBAGE_GEN_ROOT_PATH_BASE + "/" + GARBAGE_GEN_ROOT_NODE_NAME;
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String GEN_BASE_PATH = "GenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String GEN_PARENT_NODE_PREFIX = "Parent_";
+    public static String GEN_NODE_PREFIX = "Node_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node FULLGC_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public GenerateFullGCOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public GenerateFullGCOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {
+
+        try (Closer closer = Closer.create()) {
+            execute(closer, args);
+        } catch (Throwable e) {
+            LOG.error("Command failed", e);
+            throw e;
+        }
+    }
+
+    /**
+     * Method with passed closer is necessary in order to allow for unit tests to check the output of the command.
+     * It is the responsibility of the caller to close the closer.
+     *
+     * Returns the list of generated garbage base paths (under the garbage root node).
+     * @param closer
+     * @param args
+     * @throws Exception
+     */
+    public List<String> execute(Closer closer, String... args) throws Exception {
+        continuousRunIndex = 0;
+
+        List<String> generateBasePaths = new ArrayList<>();
+
+        GenerateFullGCOptions options = new GenerateFullGCOptions(USAGE).parse(args);
+        String subCmd = options.getSubCmd();
+
+        if (GenerateFullGCOptions.CMD_GENERATE.equals(subCmd)) {
+
+            if (options.getCreateGarbageNodesCount() <= 0) {
+                LOG.error("Invalid garbageNodesCount specified: " + options.getCreateGarbageNodesCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+            if (options.getGarbageNodesParentCount() <= 0) {
+                LOG.error("Invalid garbageNodesParentCount specified: " + options.getGarbageNodesParentCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+
+            if (options.getNumberOfRuns() > 1 && options.getGenerateIntervalSeconds() > 0) {
+                generateBasePaths.addAll(generateGarbageContinuously(options, closer));
+            } else {
+                generateBasePaths.add(generateGarbage(options, closer, 0));
+            }
+        } else if (GenerateFullGCOptions.CMD_CLEAN.equals(subCmd)) {
+            cleanGarbage(options, closer);
+        } else {
+            System.err.println("Unknown revisions command: " + subCmd);
+        }
+
+        return generateBasePaths;
+    }
+
+    @Override
+    public void close() throws IOException {
+        new ExecutorCloser(this.continuousRunExecutor).close();
+    }
+
+    private List<String> generateGarbageContinuously(GenerateFullGCOptions options, Closer closer) throws Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long startGenTimestamp = System.currentTimeMillis();
+
+        List<String> generatedGarbageBasePaths = new ArrayList<>();
+
+        int numberOfRuns = options.getNumberOfRuns();
+        int intervalSeconds = options.getGenerateIntervalSeconds();
+        Runnable task = () -> {
+            if (continuousRunIndex < numberOfRuns) {
+                try {
+                    String genBasePath = generateGarbage(options, closer, continuousRunIndex, builder, startGenTimestamp);
+                    generatedGarbageBasePaths.add(genBasePath);
+                } catch (Exception e) {
+                    LOG.error("Error generating garbage in run " + continuousRunIndex, e);
+                }
+                LOG.info("Task executed. Count: " + (continuousRunIndex + 1));
+                continuousRunIndex++;
+            } else {
+                // Shutdown the executor once the task has run numberOfRuns times
+                continuousRunExecutor.shutdown();
+                LOG.info("Task completed " + numberOfRuns + " times. Stopping execution.");
+            }
+        };
+
+        // Schedule the task to run every intervalSeconds
+        continuousRunExecutor.scheduleAtFixedRate(task, 0, intervalSeconds, TimeUnit.SECONDS);
+
+        return generatedGarbageBasePaths;
+    }
+
+    /**
+     * Generate garbage nodes in the repository in order to allow for testing fullGC functionality.
+     *
+     * Returns the path of the generated FULLGC_GEN_BASE_PATH node (under the root).
+     * @param options
+     * @param closer
+     * @param runIndex
+     * @return
+     * @throws IOException
+     * @throws Exception
+     */
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long generationTimestamp = System.currentTimeMillis();
+
+        return generateGarbage(options, closer, runIndex, builder, generationTimestamp);
+    }
+
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex,
+                                  DocumentNodeStoreBuilder<?> builder, long timestamp) throws IOException, Exception {
+
+        if (builder == null) {
+            System.err.println("generateFullGC mode only available for DocumentNodeStore");
+            System.exit(1);
+        }
+
+        String generationBasePath = GEN_BASE_PATH + timestamp + "_" + runIndex;
+        System.out.println("Generating fullGC on the document: " + generationBasePath);
+        documentNodeStore = builder.build();
+
+        //VersionGarbageCollector.FullGCMode fullGCMode = getFullGCMode(options);
+        if (GenerateGarbageHelper.isInvalidGarbageGenerationMode(options.getGarbageType())) {
+            LOG.error("Invalid fullGCMode specified: " + options.getGarbageType() + " in: " + getClass().getName());
+            System.exit(1);
+        }
+
+        //1. Create nodes with properties
+        NodeBuilder rootNode = documentNodeStore.getRoot().builder();
+        NodeBuilder garbageRootNode = rootNode.child(GARBAGE_GEN_ROOT_PATH_BASE).child(GARBAGE_GEN_ROOT_NODE_NAME);
+        garbageRootNode.child(generationBasePath).setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+
+        int nodesCountUnderParent = options.getCreateGarbageNodesCount() / options.getGarbageNodesParentCount();
+        for(int i = 0; i < options.getGarbageNodesParentCount(); i ++) {
+            garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).setProperty(JcrConstants.JCR_PRIMARYTYPE, "nt:folder", NAME);
+
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                        setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+
+                if (GenerateGarbageHelper.includesEmptyProps(options.getGarbageType())) {
+                    garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                            setProperty(EMPTY_PROPERTY_NAME, "bar", STRING);
+                }
+            }
+        }
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+
+
+        //2. Generate garbage nodes - EMPTY_PROPERTIES
+        if (GenerateGarbageHelper.includesEmptyProps(options.getGarbageType())) {
+            for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+                for (int j = 0; j < nodesCountUnderParent; j++) {
+                    garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                            removeProperty(EMPTY_PROPERTY_NAME);
+                }
+            }
+        }
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+
+        //3.1. Generate garbage nodes - GAP_ORPHANS - remove parent nodes
+        if (GenerateGarbageHelper.includesGapOrphans(options.getGarbageType())) {

Review Comment:
   All these implementations could be saved inside a `Map` of `fullGcMode --> Implementation` and we can simply iterate over garbageType and create garbage of different types in one simple loop.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "rishabhdaim (via GitHub)" <gi...@apache.org>.
rishabhdaim commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1808494898


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateFullGCCommand.java:
##########
@@ -0,0 +1,416 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.plugins.document.Collection;
+import org.apache.jackrabbit.oak.plugins.document.Document;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.DocumentStoreException;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateFullGCCommand implements Command {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateFullGCCommand.class);
+
+    private static final String USAGE = "generateFullGC {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String FULLGC_GEN_ROOT_PATH = "fullGCGenRoot";

Review Comment:
   how about we use `garbage` to make it clear than we are creating garbage under this folder.



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateFullGCCommand.java:
##########
@@ -0,0 +1,416 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.plugins.document.Collection;
+import org.apache.jackrabbit.oak.plugins.document.Document;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.DocumentStoreException;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateFullGCCommand implements Command {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateFullGCCommand.class);
+
+    private static final String USAGE = "generateFullGC {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String FULLGC_GEN_ROOT_PATH = "fullGCGenRoot";
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String FULLGC_GEN_BASE_PATH = "fullGCGenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String FULLGC_GEN_PARENT_NODE_PREFIX = "fullGCParent_";
+    public static String FULLGC_GEN_NODE_PREFIX = "fullGCNode_";

Review Comment:
   I would avoid using FULLGC in variable names.



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateFullGCCommand.java:
##########
@@ -0,0 +1,416 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.plugins.document.Collection;
+import org.apache.jackrabbit.oak.plugins.document.Document;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.DocumentStoreException;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateFullGCCommand implements Command {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateFullGCCommand.class);
+
+    private static final String USAGE = "generateFullGC {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String FULLGC_GEN_ROOT_PATH = "fullGCGenRoot";
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String FULLGC_GEN_BASE_PATH = "fullGCGenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String FULLGC_GEN_PARENT_NODE_PREFIX = "fullGCParent_";
+    public static String FULLGC_GEN_NODE_PREFIX = "fullGCNode_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node FULLGC_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public GenerateFullGCOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public GenerateFullGCOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {

Review Comment:
   @dare-gartners we are overriding the `execute` method from `Command` interface here. We have to follow the syntax.



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateFullGCCommand.java:
##########
@@ -0,0 +1,416 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.plugins.document.Collection;
+import org.apache.jackrabbit.oak.plugins.document.Document;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.DocumentStoreException;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateFullGCCommand implements Command {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateFullGCCommand.class);
+
+    private static final String USAGE = "generateFullGC {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String FULLGC_GEN_ROOT_PATH = "fullGCGenRoot";
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String FULLGC_GEN_BASE_PATH = "fullGCGenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String FULLGC_GEN_PARENT_NODE_PREFIX = "fullGCParent_";
+    public static String FULLGC_GEN_NODE_PREFIX = "fullGCNode_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node FULLGC_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public GenerateFullGCOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public GenerateFullGCOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {
+        Closer closer = Closer.create();
+        try {
+            execute(closer, args);
+        } catch (Throwable e) {
+            LOG.error("Command failed", e);
+            throw closer.rethrow(e);
+        } finally {
+            closer.close();
+        }
+    }
+
+    /**
+     * Method with passed closer is necessary in order to allow for unit tests to check the output of the command.
+     * It is the responsibility of the caller to close the closer.
+     *
+     * Returns the list of generated garbage base paths (under the garbage root node).
+     * @param closer
+     * @param args
+     * @throws Exception
+     */
+    public List<String> execute(Closer closer, String... args) throws Exception {
+        continuousRunIndex = 0;
+
+        List<String> generateBasePaths = new ArrayList<>();
+
+        GenerateFullGCOptions options = new GenerateFullGCOptions(USAGE).parse(args);
+        String subCmd = options.getSubCmd();
+
+        if (GenerateFullGCOptions.CMD_GENERATE.equals(subCmd)) {
+            if (options.getNumberOfRuns() > 1 && options.getGenerateIntervalSeconds() > 0) {
+                generateBasePaths.addAll(generateGarbageContinuously(options, closer));
+            } else {
+                generateBasePaths.add(generateGarbage(options, closer, 0));
+            }
+        } else if (GenerateFullGCOptions.CMD_CLEAN.equals(subCmd)) {
+            cleanGarbage(options, closer);
+        } else {
+            System.err.println("unknown revisions command: " + subCmd);
+        }
+
+        return generateBasePaths;
+    }
+
+    private List<String> generateGarbageContinuously(GenerateFullGCOptions options, Closer closer) throws IOException, Exception {
+        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long startGenTimestamp = System.currentTimeMillis();
+
+        List<String> generatedGarbageBasePaths = new ArrayList<>();
+
+        int numberOfRuns = options.getNumberOfRuns();
+        int intervalSeconds = options.getGenerateIntervalSeconds();
+        Runnable task = () -> {
+            if (continuousRunIndex < numberOfRuns) {
+                try {
+                    String genBasePath = generateGarbage(options, closer, continuousRunIndex, builder, startGenTimestamp);
+                    generatedGarbageBasePaths.add(genBasePath);
+                } catch (Exception e) {
+                    LOG.error("Error generating garbage in run " + continuousRunIndex, e);
+                }
+                LOG.info("Task executed. Count: " + (continuousRunIndex + 1));
+                continuousRunIndex++;
+            } else {
+                // Shutdown the executor once the task has run numberOfRuns times
+                executor.shutdown();
+                LOG.info("Task completed " + numberOfRuns + " times. Stopping execution.");
+            }
+        };
+
+        // Schedule the task to run every intervalSeconds
+        executor.scheduleAtFixedRate(task, 0, intervalSeconds, TimeUnit.SECONDS);
+
+        return generatedGarbageBasePaths;
+    }
+
+    /**
+     * Generate garbage nodes in the repository in order to allow for testing fullGC functionality.
+     *
+     * Returns the path of the generated FULLGC_GEN_BASE_PATH node (under the root).
+     * @param options
+     * @param closer
+     * @param runIndex
+     * @return
+     * @throws IOException
+     * @throws Exception
+     */
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long generationTimestamp = System.currentTimeMillis();
+
+        return generateGarbage(options, closer, runIndex, builder, generationTimestamp);
+    }
+
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex,
+                                  DocumentNodeStoreBuilder<?> builder, long timestamp) throws IOException, Exception {
+
+        if (builder == null) {
+            System.err.println("generateFullGC mode only available for DocumentNodeStore");
+            System.exit(1);
+        }
+
+        String generationBasePath = FULLGC_GEN_BASE_PATH + timestamp + "_" + runIndex;
+        System.out.println("Generating fullGC on the document: " + generationBasePath);
+        documentNodeStore = builder.build();
+
+        VersionGarbageCollector.FullGCMode fullGCMode = getFullGCMode(options);
+        if (fullGCMode == VersionGarbageCollector.FullGCMode.NONE) {
+            LOG.error("Invalid garbageType specified. Must be one of the following: 1 - EMPTYPROPS, 2 - GAP_ORPHANS, 3 - GAP_ORPHANS_EMPTYPROPS");
+            System.exit(1);
+        }
+
+        //1. Create nodes with properties
+        NodeBuilder rootNode = documentNodeStore.getRoot().builder();
+        NodeBuilder garbageRootNode = rootNode.child(FULLGC_GEN_ROOT_PATH);
+        garbageRootNode.child(generationBasePath).setProperty("jcr:primaryType", "nt:file", NAME);
+
+        int nodesCountUnderParent = options.getCreateGarbageNodesCount() / options.getGarbageNodesParentCount();

Review Comment:
   Yes, this is entirely possible. Good catch @dare-gartners.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "shodaaan (via GitHub)" <gi...@apache.org>.
shodaaan commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1809020847


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateFullGCCommand.java:
##########
@@ -0,0 +1,416 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.plugins.document.Collection;
+import org.apache.jackrabbit.oak.plugins.document.Document;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.DocumentStoreException;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateFullGCCommand implements Command {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateFullGCCommand.class);
+
+    private static final String USAGE = "generateFullGC {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String FULLGC_GEN_ROOT_PATH = "fullGCGenRoot";
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String FULLGC_GEN_BASE_PATH = "fullGCGenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String FULLGC_GEN_PARENT_NODE_PREFIX = "fullGCParent_";
+    public static String FULLGC_GEN_NODE_PREFIX = "fullGCNode_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node FULLGC_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public GenerateFullGCOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public GenerateFullGCOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {
+        Closer closer = Closer.create();
+        try {

Review Comment:
   Done, thank you for the suggestion.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "shodaaan (via GitHub)" <gi...@apache.org>.
shodaaan commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1832474265


##########
oak-run/src/test/java/org/apache/jackrabbit/oak/plugins/document/CreateGarbageCommandTest.java:
##########
@@ -0,0 +1,437 @@
+/*
+ * 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 org.apache.jackrabbit.guava.common.collect.ImmutableList;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.plugins.document.util.MongoConnection;
+import org.apache.jackrabbit.oak.plugins.document.util.Utils;
+import org.apache.jackrabbit.oak.run.CreateGarbageCommand;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.apache.jackrabbit.oak.plugins.document.CommandTestUtils.captureSystemOut;
+import static org.apache.jackrabbit.oak.run.CreateGarbageCommand.EMPTY_PROPERTY_NAME;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+public class CreateGarbageCommandTest {
+
+    final String OPTION_GARBAGE_NODES_COUNT = "--garbageNodesCount";
+    final String OPTION_GARBAGE_NODES_PARENT_COUNT = "--garbageNodesParentCount";
+    final String OPTION_GARBAGE_TYPE = "--garbageType";
+    final String OPTION_GARBAGE_ORPHANS_DEPTH = "--orphansDepth";
+    final String OPTION_GARBAGE_ORPHANS_LEVEL_GAP = "--orphansLevelGap";
+    final String OPTION_NUMBER_OF_RUNS = "--numberOfRuns";
+    final String OPTION_GENERATE_INTERVAL_SECONDS = "--generateIntervalSeconds";
+
+    @Rule
+    public MongoConnectionFactory connectionFactory = new MongoConnectionFactory();
+
+    @Rule
+    public DocumentMKBuilderProvider builderProvider = new DocumentMKBuilderProvider();
+
+    private DocumentNodeStore ns;
+
+    private Closer closer;
+
+    @BeforeClass
+    public static void assumeMongoDB() {
+        assumeTrue(MongoUtils.isAvailable());
+    }
+
+    @Before
+    public void before() {
+        ns = createDocumentNodeStore();
+    }
+
+    @After
+    public void after() {
+        try {
+            closer.close();
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+        ns.dispose();
+    }
+
+    private DocumentNodeStore createDocumentNodeStore() {
+        MongoConnection c = connectionFactory.getConnection();
+        assertNotNull(c);
+        MongoUtils.dropCollections(c.getDatabase());
+        return builderProvider.newBuilder().setFullGCEnabled(false).setLeaseCheckMode(LeaseCheckMode.DISABLED).setAsyncDelay(0)
+                .setMongoDB(c.getMongoClient(), c.getDBName()).getNodeStore();
+    }
+
+    private List<String> getChildNodeNames(NodeState node) {
+        List<String> childNodeNames = new ArrayList<>();
+        for (String childNodeName : node.getChildNodeNames()) {
+            childNodeNames.add(childNodeName);
+        }
+        return childNodeNames;
+    }
+
+    /**
+     * This test generates garbage empty properties under one parent node and then cleans up the garbage.
+     * Use a custom MongoDB connection string for the MongoDB to run the test on.
+     * Long running test, should only be run manually.
+     */
+    @Ignore
+    @Test
+    public void generateGarbageEmptyPropsUnderOneParentOnCustomMongoDB() {
+        ns.dispose();
+
+        // this should be a valid mongoDB connection string for the MongoDB on which to generate the garbage
+        String mongoDBConnString = "";
+        CreateGarbageCmd cmd = new CreateGarbageCmd( mongoDBConnString, OPTION_GARBAGE_NODES_COUNT, "10", OPTION_GARBAGE_NODES_PARENT_COUNT, "1",
+                OPTION_GARBAGE_TYPE, "1");
+        String output = captureSystemOut(cmd);
+
+        //sleep thread for 10 seconds to allow the garbage generation to complete
+        try {
+            Thread.sleep(10000);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+
+        DocumentNodeStore nodeStore = cmd.getCommand().getDocumentNodeStore();
+        NodeState garbageRootNodeState = getGarbageRootNode(nodeStore);
+        List<String> garbageRootNodeChildNames = getChildNodeNames(garbageRootNodeState);
+
+        assertEquals(garbageRootNodeChildNames.size(), 1);
+        List<String> propParentsGarbageNodeNames = getChildNodeNames(garbageRootNodeState.getChildNode(garbageRootNodeChildNames.get(0)));
+        assertEquals(propParentsGarbageNodeNames.size(), 1);
+
+        List<String> propGarbageNodeNames = getChildNodeNames(garbageRootNodeState.getChildNode(garbageRootNodeChildNames.get(0)).getChildNode(propParentsGarbageNodeNames.get(0)));
+        assertEquals(propGarbageNodeNames.size(), 10);
+    }
+
+    /**
+     * This test generates garbage empty properties and then cleans up the garbage.
+     * Long running test, should only be run manually.
+     */
+    @Ignore
+    @Test
+    public void cleanupAllGarbageEmptyProperties() {
+
+        // generate garbage first
+        CreateGarbageCmd cmdGenerateGarbage = new CreateGarbageCmd(OPTION_GARBAGE_NODES_COUNT, "4", OPTION_GARBAGE_NODES_PARENT_COUNT, "3",
+                OPTION_GARBAGE_TYPE, "1");
+        cmdGenerateGarbage.run();
+        Closer cmd1Closer = cmdGenerateGarbage.getCloser();
+
+        // cleanup garbage
+        CreateGarbageCmd cmdClean = new CreateGarbageCmd("clean");
+        cmdClean.run();
+        closer = cmdClean.getCloser();
+
+        NodeDocument garbageRoot = getDocument(cmdClean, Collection.NODES, Utils.getIdFromPath("/" + CreateGarbageCommand.GARBAGE_GEN_ROOT_PATH), 0);
+        // garbage root node should be either deleted or empty (no children)
+        assertTrue(garbageRoot == null || !garbageRoot.hasChildren());
+
+        try {
+            cmd1Closer.close();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * This test generates garbage gap orphans and then cleans up the garbage.
+     * Long running test, should only be run manually.
+     */
+    @Ignore
+    @Test
+    public void cleanupAllGarbageGapOrphans() {
+
+        // generate garbage first
+        CreateGarbageCmd cmdGenerateGarbage = new CreateGarbageCmd(OPTION_GARBAGE_NODES_COUNT, "10", OPTION_GARBAGE_NODES_PARENT_COUNT, "1",
+                OPTION_GARBAGE_TYPE, "2");
+        cmdGenerateGarbage.run();
+        Closer cmd1Closer = cmdGenerateGarbage.getCloser();
+
+        // cleanup garbage
+        CreateGarbageCmd cmdClean = new CreateGarbageCmd("clean");
+        cmdClean.run();
+        closer = cmdClean.getCloser();
+
+        NodeDocument garbageRoot = getDocument(cmdClean, Collection.NODES, Utils.getIdFromPath("/" + CreateGarbageCommand.GARBAGE_GEN_ROOT_PATH), 0);
+        // garbage root node should be either deleted or empty (no children)
+        assertTrue(garbageRoot == null || !garbageRoot.hasChildren());
+
+        try {
+            cmd1Closer.close();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Test
+    public void generateGarbageEmptyPropsUnderOneParent() {
+        ns.dispose();
+
+        CreateGarbageCmd cmd = new CreateGarbageCmd(OPTION_GARBAGE_NODES_COUNT, "10", OPTION_GARBAGE_NODES_PARENT_COUNT, "1",
+                OPTION_GARBAGE_TYPE, "1");
+        String output = captureSystemOut(cmd);
+        closer = cmd.getCloser();
+
+        List<String> generatedBasePaths = cmd.getGeneratedBasePaths();
+
+        DocumentNodeStore nodeStore = cmd.getCommand().getDocumentNodeStore();
+        NodeState garbageRootNodeState = getGarbageRootNode(nodeStore);
+        List<String> garbageRootNodeChildNames = getChildNodeNames(garbageRootNodeState);
+
+        assertEquals(garbageRootNodeChildNames.size(), 1);
+        for (String generateBasePath : generatedBasePaths) {
+            assertTrue(garbageRootNodeChildNames.contains(generateBasePath));
+        }
+
+        List<String> propParentsGarbageNodeNames = getChildNodeNames(garbageRootNodeState.getChildNode(garbageRootNodeChildNames.get(0)));
+        assertEquals(propParentsGarbageNodeNames.size(), 1);
+
+        NodeState emptyPropsParentNode = garbageRootNodeState.getChildNode(garbageRootNodeChildNames.get(0)).getChildNode(propParentsGarbageNodeNames.get(0));
+        List<String> propGarbageNodeNames = getChildNodeNames(emptyPropsParentNode);
+        assertEquals(propGarbageNodeNames.size(), 10);
+
+        for (String propGarbageNodeName : propGarbageNodeNames) {
+            NodeState propGarbageNode = emptyPropsParentNode.getChildNode(propGarbageNodeName);
+            assertNull(propGarbageNode.getProperty(EMPTY_PROPERTY_NAME));
+        }
+    }
+
+    @Test
+    public void generateGarbageGapOrphansUnderOneParent() {
+        ns.dispose();
+
+        int garbageNodesCount = 10;
+        int garbageNodesParentCount = 1;
+
+        testGenerateGapOrphans(garbageNodesCount, garbageNodesParentCount);
+    }
+
+    @Test
+    public void generateGarbageGapOrphansUnderMultipleParents() {
+        ns.dispose();
+
+        int garbageNodesCount = 20;
+        int garbageNodesParentCount = 4;
+
+        testGenerateGapOrphans(garbageNodesCount, garbageNodesParentCount);
+    }
+
+    @Test
+    public void generateGarbageGapOrphansWithDepthAndMinLevelGap() {
+        ns.dispose();
+
+        int garbageNodesCount = 5;
+        int garbageNodesParentCount = 1;
+        int orphansDepth = 7;
+        int orphansLevelGap = 0;
+
+        testGenerateGapOrphans(garbageNodesCount, garbageNodesParentCount, orphansDepth, orphansLevelGap);
+    }
+
+    @Test
+    public void generateGarbageGapOrphansWithDepthAndMaxLevelMinusOneGap() {
+        ns.dispose();
+
+        int garbageNodesCount = 1;
+        int garbageNodesParentCount = 1;
+        int orphansDepth = 10;
+        int orphansLevelGap = 9;
+
+        testGenerateGapOrphans(garbageNodesCount, garbageNodesParentCount, orphansDepth, orphansLevelGap);
+    }
+
+    @Test
+    public void generateGarbageGapOrphansWithDepthAndMaxLevelGap() {
+        ns.dispose();
+
+        int garbageNodesCount = 5;
+        int garbageNodesParentCount = 1;
+        int orphansDepth = 11;
+        int orphansLevelGap = 11;
+
+        testGenerateGapOrphans(garbageNodesCount, garbageNodesParentCount, orphansDepth, orphansLevelGap);
+    }
+
+    @Test
+    public void generateGarbageGapOrphansWithDepthAndMiddleLevelGap() {
+        ns.dispose();
+
+        int garbageNodesCount = 5;
+        int garbageNodesParentCount = 1;
+        int orphansDepth = 8;
+        int orphansLevelGap = 4;
+
+        testGenerateGapOrphans(garbageNodesCount, garbageNodesParentCount, orphansDepth, orphansLevelGap);
+    }
+
+    /**
+     * Calls the GenerateFullGCCommand to generate gap orphans and verifies the generated gap orphans were created
+     * correctly.
+     * Gap orphan depth is minimum and level gap is 0.
+     * @param garbageNodesCount
+     * @param garbageNodesParentCount
+     */
+    private void testGenerateGapOrphans(int garbageNodesCount, int garbageNodesParentCount) {
+        testGenerateGapOrphans(garbageNodesCount, garbageNodesParentCount, 1, 0);
+    }
+
+    /**
+     * Calls the GenerateFullGCCommand to generate gap orphans and verifies the generated gap orphans:
+     * - checks that parents of gap orphans are missing by retrieving the nodes from the document store
+     * - checks that the expected number gap orphans are generated by retrieving the nodes from the document store
+     * @param garbageNodesCount
+     * @param garbageNodesParentCount
+     * @param orphansDepth - the depth of the leaf garbage gap orphan node
+     * @param orphansLevelGap - the level that is used as threshold for deletion - all nodes from the parent to this node
+     *                        will be deleted
+     */
+    private void testGenerateGapOrphans(int garbageNodesCount, int garbageNodesParentCount,
+                                        int orphansDepth, int orphansLevelGap) {
+        CreateGarbageCmd cmd = new CreateGarbageCmd(OPTION_GARBAGE_NODES_COUNT, String.valueOf(garbageNodesCount),
+                OPTION_GARBAGE_NODES_PARENT_COUNT, String.valueOf(garbageNodesParentCount),
+                OPTION_GARBAGE_ORPHANS_DEPTH, String.valueOf(orphansDepth),
+                OPTION_GARBAGE_ORPHANS_LEVEL_GAP, String.valueOf(orphansLevelGap),
+                OPTION_GARBAGE_TYPE, "2");
+        String output = captureSystemOut(cmd);
+        closer = cmd.getCloser();
+
+        int nodesPerParent = garbageNodesCount / garbageNodesParentCount;
+
+        List<String> generatedBasePaths = cmd.getGeneratedBasePaths();
+
+        DocumentNodeStore nodeStore = cmd.getCommand().getDocumentNodeStore();
+        NodeState garbageRootNodeState = getGarbageRootNode(nodeStore);
+        List<String> garbageRootNodeChildNames = getChildNodeNames(garbageRootNodeState);
+
+        assertEquals(garbageRootNodeChildNames.size(), 1);
+        for (String generateBasePath : generatedBasePaths) {
+            assertTrue(garbageRootNodeChildNames.contains(generateBasePath));
+        }
+
+        String generateBasePath = generatedBasePaths.get(0);
+        for (int i = 0; i < garbageNodesParentCount; i ++) {
+
+            for (int j = 0; j < nodesPerParent; j++) {
+
+                // check deleted node paths
+                List<String> deletedNodePaths = CreateGarbageCommand.generateGapOrphanNodePaths(
+                        generateBasePath, i, j, orphansDepth, orphansLevelGap, false);
+                for (String deletedNodePath : deletedNodePaths) {
+                    NodeDocument missingDocument = getDocument(cmd, Collection.NODES, deletedNodePath, 0);
+                    assertNull(missingDocument);
+                }
+
+                // check not deleted node paths - remaining gap orphans
+                List<String> notDeletedNodePaths = CreateGarbageCommand.generateGapOrphanNodePaths(generateBasePath, i, j, orphansDepth, orphansLevelGap, true);
+                for (String notDeletedNodePath : notDeletedNodePaths) {
+                    NodeDocument gapOrphanDocument = getDocument(cmd, Collection.NODES, notDeletedNodePath, 0);
+                    assertNotNull(gapOrphanDocument);
+                }
+            }
+        }
+    }
+
+    /**
+     * Utility method that returns the root node of the full gc generated garbage nodes.
+     * @param nodeStore
+     * @return
+     */
+    private static @NotNull NodeState getGarbageRootNode(DocumentNodeStore nodeStore) {
+        NodeState garbageRootNodeState = nodeStore.getRoot().getChildNode(CreateGarbageCommand.GARBAGE_GEN_ROOT_PATH_BASE).
+                getChildNode(CreateGarbageCommand.GARBAGE_GEN_ROOT_NODE_NAME);
+        return garbageRootNodeState;
+    }
+
+    /**
+     * Used in unit tests for retrieving a document by path from the document store using the documentNodeStore
+     * of this class.
+     * @param collection
+     * @param key
+     * @param maxCacheAge
+     * @return
+     * @param <T>
+     * @throws DocumentStoreException
+     */
+    @Nullable
+    private  <T extends Document> T getDocument(CreateGarbageCmd cmd, Collection<T> collection, String key, int maxCacheAge)
+            throws DocumentStoreException {
+        return cmd.getCommand().getDocumentNodeStore().getDocumentStore().find(collection, key, maxCacheAge);
+    }
+
+    /**
+     * Wrapper class for the GenerateGarbageCommand to be used in unit tests.
+     */
+    private static class CreateGarbageCmd implements Runnable {
+
+        private final ImmutableList<String> args;
+        private CreateGarbageCommand command;
+        private Closer closer;
+        private List<String> generatedBasePaths;
+
+        public CreateGarbageCmd(String... args) {
+            // append the default mongodb connection string if one was not provided
+            if (args[0].startsWith("mongodb://")) {
+                this.args = ImmutableList.copyOf(args);

Review Comment:
   Done, thank you for the suggestion.



##########
oak-run/src/test/java/org/apache/jackrabbit/oak/plugins/document/CreateGarbageCommandTest.java:
##########
@@ -0,0 +1,437 @@
+/*
+ * 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 org.apache.jackrabbit.guava.common.collect.ImmutableList;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.plugins.document.util.MongoConnection;
+import org.apache.jackrabbit.oak.plugins.document.util.Utils;
+import org.apache.jackrabbit.oak.run.CreateGarbageCommand;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.apache.jackrabbit.oak.plugins.document.CommandTestUtils.captureSystemOut;
+import static org.apache.jackrabbit.oak.run.CreateGarbageCommand.EMPTY_PROPERTY_NAME;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+public class CreateGarbageCommandTest {
+
+    final String OPTION_GARBAGE_NODES_COUNT = "--garbageNodesCount";
+    final String OPTION_GARBAGE_NODES_PARENT_COUNT = "--garbageNodesParentCount";
+    final String OPTION_GARBAGE_TYPE = "--garbageType";
+    final String OPTION_GARBAGE_ORPHANS_DEPTH = "--orphansDepth";
+    final String OPTION_GARBAGE_ORPHANS_LEVEL_GAP = "--orphansLevelGap";
+    final String OPTION_NUMBER_OF_RUNS = "--numberOfRuns";
+    final String OPTION_GENERATE_INTERVAL_SECONDS = "--generateIntervalSeconds";
+
+    @Rule
+    public MongoConnectionFactory connectionFactory = new MongoConnectionFactory();
+
+    @Rule
+    public DocumentMKBuilderProvider builderProvider = new DocumentMKBuilderProvider();
+
+    private DocumentNodeStore ns;
+
+    private Closer closer;
+
+    @BeforeClass
+    public static void assumeMongoDB() {
+        assumeTrue(MongoUtils.isAvailable());
+    }
+
+    @Before
+    public void before() {
+        ns = createDocumentNodeStore();
+    }
+
+    @After
+    public void after() {
+        try {
+            closer.close();
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+        ns.dispose();
+    }
+
+    private DocumentNodeStore createDocumentNodeStore() {
+        MongoConnection c = connectionFactory.getConnection();
+        assertNotNull(c);
+        MongoUtils.dropCollections(c.getDatabase());
+        return builderProvider.newBuilder().setFullGCEnabled(false).setLeaseCheckMode(LeaseCheckMode.DISABLED).setAsyncDelay(0)
+                .setMongoDB(c.getMongoClient(), c.getDBName()).getNodeStore();
+    }
+
+    private List<String> getChildNodeNames(NodeState node) {
+        List<String> childNodeNames = new ArrayList<>();
+        for (String childNodeName : node.getChildNodeNames()) {
+            childNodeNames.add(childNodeName);
+        }
+        return childNodeNames;
+    }
+
+    /**
+     * This test generates garbage empty properties under one parent node and then cleans up the garbage.
+     * Use a custom MongoDB connection string for the MongoDB to run the test on.
+     * Long running test, should only be run manually.
+     */
+    @Ignore
+    @Test
+    public void generateGarbageEmptyPropsUnderOneParentOnCustomMongoDB() {
+        ns.dispose();
+
+        // this should be a valid mongoDB connection string for the MongoDB on which to generate the garbage
+        String mongoDBConnString = "";
+        CreateGarbageCmd cmd = new CreateGarbageCmd( mongoDBConnString, OPTION_GARBAGE_NODES_COUNT, "10", OPTION_GARBAGE_NODES_PARENT_COUNT, "1",
+                OPTION_GARBAGE_TYPE, "1");
+        String output = captureSystemOut(cmd);
+
+        //sleep thread for 10 seconds to allow the garbage generation to complete
+        try {
+            Thread.sleep(10000);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+
+        DocumentNodeStore nodeStore = cmd.getCommand().getDocumentNodeStore();
+        NodeState garbageRootNodeState = getGarbageRootNode(nodeStore);
+        List<String> garbageRootNodeChildNames = getChildNodeNames(garbageRootNodeState);
+
+        assertEquals(garbageRootNodeChildNames.size(), 1);
+        List<String> propParentsGarbageNodeNames = getChildNodeNames(garbageRootNodeState.getChildNode(garbageRootNodeChildNames.get(0)));
+        assertEquals(propParentsGarbageNodeNames.size(), 1);
+
+        List<String> propGarbageNodeNames = getChildNodeNames(garbageRootNodeState.getChildNode(garbageRootNodeChildNames.get(0)).getChildNode(propParentsGarbageNodeNames.get(0)));
+        assertEquals(propGarbageNodeNames.size(), 10);
+    }
+
+    /**
+     * This test generates garbage empty properties and then cleans up the garbage.
+     * Long running test, should only be run manually.
+     */
+    @Ignore
+    @Test
+    public void cleanupAllGarbageEmptyProperties() {
+
+        // generate garbage first
+        CreateGarbageCmd cmdGenerateGarbage = new CreateGarbageCmd(OPTION_GARBAGE_NODES_COUNT, "4", OPTION_GARBAGE_NODES_PARENT_COUNT, "3",
+                OPTION_GARBAGE_TYPE, "1");
+        cmdGenerateGarbage.run();
+        Closer cmd1Closer = cmdGenerateGarbage.getCloser();
+
+        // cleanup garbage
+        CreateGarbageCmd cmdClean = new CreateGarbageCmd("clean");
+        cmdClean.run();
+        closer = cmdClean.getCloser();
+
+        NodeDocument garbageRoot = getDocument(cmdClean, Collection.NODES, Utils.getIdFromPath("/" + CreateGarbageCommand.GARBAGE_GEN_ROOT_PATH), 0);
+        // garbage root node should be either deleted or empty (no children)
+        assertTrue(garbageRoot == null || !garbageRoot.hasChildren());
+
+        try {
+            cmd1Closer.close();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * This test generates garbage gap orphans and then cleans up the garbage.
+     * Long running test, should only be run manually.
+     */
+    @Ignore
+    @Test
+    public void cleanupAllGarbageGapOrphans() {
+
+        // generate garbage first
+        CreateGarbageCmd cmdGenerateGarbage = new CreateGarbageCmd(OPTION_GARBAGE_NODES_COUNT, "10", OPTION_GARBAGE_NODES_PARENT_COUNT, "1",
+                OPTION_GARBAGE_TYPE, "2");
+        cmdGenerateGarbage.run();
+        Closer cmd1Closer = cmdGenerateGarbage.getCloser();
+
+        // cleanup garbage
+        CreateGarbageCmd cmdClean = new CreateGarbageCmd("clean");
+        cmdClean.run();
+        closer = cmdClean.getCloser();
+
+        NodeDocument garbageRoot = getDocument(cmdClean, Collection.NODES, Utils.getIdFromPath("/" + CreateGarbageCommand.GARBAGE_GEN_ROOT_PATH), 0);
+        // garbage root node should be either deleted or empty (no children)
+        assertTrue(garbageRoot == null || !garbageRoot.hasChildren());
+
+        try {
+            cmd1Closer.close();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Test
+    public void generateGarbageEmptyPropsUnderOneParent() {
+        ns.dispose();
+
+        CreateGarbageCmd cmd = new CreateGarbageCmd(OPTION_GARBAGE_NODES_COUNT, "10", OPTION_GARBAGE_NODES_PARENT_COUNT, "1",
+                OPTION_GARBAGE_TYPE, "1");
+        String output = captureSystemOut(cmd);
+        closer = cmd.getCloser();
+
+        List<String> generatedBasePaths = cmd.getGeneratedBasePaths();
+
+        DocumentNodeStore nodeStore = cmd.getCommand().getDocumentNodeStore();
+        NodeState garbageRootNodeState = getGarbageRootNode(nodeStore);
+        List<String> garbageRootNodeChildNames = getChildNodeNames(garbageRootNodeState);
+
+        assertEquals(garbageRootNodeChildNames.size(), 1);
+        for (String generateBasePath : generatedBasePaths) {
+            assertTrue(garbageRootNodeChildNames.contains(generateBasePath));
+        }
+
+        List<String> propParentsGarbageNodeNames = getChildNodeNames(garbageRootNodeState.getChildNode(garbageRootNodeChildNames.get(0)));
+        assertEquals(propParentsGarbageNodeNames.size(), 1);
+
+        NodeState emptyPropsParentNode = garbageRootNodeState.getChildNode(garbageRootNodeChildNames.get(0)).getChildNode(propParentsGarbageNodeNames.get(0));
+        List<String> propGarbageNodeNames = getChildNodeNames(emptyPropsParentNode);
+        assertEquals(propGarbageNodeNames.size(), 10);
+
+        for (String propGarbageNodeName : propGarbageNodeNames) {
+            NodeState propGarbageNode = emptyPropsParentNode.getChildNode(propGarbageNodeName);
+            assertNull(propGarbageNode.getProperty(EMPTY_PROPERTY_NAME));
+        }
+    }
+
+    @Test
+    public void generateGarbageGapOrphansUnderOneParent() {
+        ns.dispose();
+
+        int garbageNodesCount = 10;
+        int garbageNodesParentCount = 1;
+
+        testGenerateGapOrphans(garbageNodesCount, garbageNodesParentCount);
+    }
+
+    @Test
+    public void generateGarbageGapOrphansUnderMultipleParents() {
+        ns.dispose();
+
+        int garbageNodesCount = 20;
+        int garbageNodesParentCount = 4;
+
+        testGenerateGapOrphans(garbageNodesCount, garbageNodesParentCount);
+    }
+
+    @Test
+    public void generateGarbageGapOrphansWithDepthAndMinLevelGap() {
+        ns.dispose();
+
+        int garbageNodesCount = 5;
+        int garbageNodesParentCount = 1;
+        int orphansDepth = 7;
+        int orphansLevelGap = 0;
+
+        testGenerateGapOrphans(garbageNodesCount, garbageNodesParentCount, orphansDepth, orphansLevelGap);
+    }
+
+    @Test
+    public void generateGarbageGapOrphansWithDepthAndMaxLevelMinusOneGap() {
+        ns.dispose();
+
+        int garbageNodesCount = 1;
+        int garbageNodesParentCount = 1;
+        int orphansDepth = 10;
+        int orphansLevelGap = 9;
+
+        testGenerateGapOrphans(garbageNodesCount, garbageNodesParentCount, orphansDepth, orphansLevelGap);
+    }
+
+    @Test
+    public void generateGarbageGapOrphansWithDepthAndMaxLevelGap() {
+        ns.dispose();
+
+        int garbageNodesCount = 5;
+        int garbageNodesParentCount = 1;
+        int orphansDepth = 11;
+        int orphansLevelGap = 11;
+
+        testGenerateGapOrphans(garbageNodesCount, garbageNodesParentCount, orphansDepth, orphansLevelGap);
+    }
+
+    @Test
+    public void generateGarbageGapOrphansWithDepthAndMiddleLevelGap() {
+        ns.dispose();
+
+        int garbageNodesCount = 5;
+        int garbageNodesParentCount = 1;
+        int orphansDepth = 8;
+        int orphansLevelGap = 4;
+
+        testGenerateGapOrphans(garbageNodesCount, garbageNodesParentCount, orphansDepth, orphansLevelGap);
+    }
+
+    /**
+     * Calls the GenerateFullGCCommand to generate gap orphans and verifies the generated gap orphans were created
+     * correctly.
+     * Gap orphan depth is minimum and level gap is 0.
+     * @param garbageNodesCount
+     * @param garbageNodesParentCount
+     */
+    private void testGenerateGapOrphans(int garbageNodesCount, int garbageNodesParentCount) {
+        testGenerateGapOrphans(garbageNodesCount, garbageNodesParentCount, 1, 0);
+    }
+
+    /**
+     * Calls the GenerateFullGCCommand to generate gap orphans and verifies the generated gap orphans:
+     * - checks that parents of gap orphans are missing by retrieving the nodes from the document store
+     * - checks that the expected number gap orphans are generated by retrieving the nodes from the document store
+     * @param garbageNodesCount
+     * @param garbageNodesParentCount
+     * @param orphansDepth - the depth of the leaf garbage gap orphan node
+     * @param orphansLevelGap - the level that is used as threshold for deletion - all nodes from the parent to this node
+     *                        will be deleted
+     */
+    private void testGenerateGapOrphans(int garbageNodesCount, int garbageNodesParentCount,
+                                        int orphansDepth, int orphansLevelGap) {
+        CreateGarbageCmd cmd = new CreateGarbageCmd(OPTION_GARBAGE_NODES_COUNT, String.valueOf(garbageNodesCount),
+                OPTION_GARBAGE_NODES_PARENT_COUNT, String.valueOf(garbageNodesParentCount),
+                OPTION_GARBAGE_ORPHANS_DEPTH, String.valueOf(orphansDepth),
+                OPTION_GARBAGE_ORPHANS_LEVEL_GAP, String.valueOf(orphansLevelGap),
+                OPTION_GARBAGE_TYPE, "2");
+        String output = captureSystemOut(cmd);
+        closer = cmd.getCloser();
+
+        int nodesPerParent = garbageNodesCount / garbageNodesParentCount;
+
+        List<String> generatedBasePaths = cmd.getGeneratedBasePaths();
+
+        DocumentNodeStore nodeStore = cmd.getCommand().getDocumentNodeStore();
+        NodeState garbageRootNodeState = getGarbageRootNode(nodeStore);
+        List<String> garbageRootNodeChildNames = getChildNodeNames(garbageRootNodeState);
+
+        assertEquals(garbageRootNodeChildNames.size(), 1);
+        for (String generateBasePath : generatedBasePaths) {
+            assertTrue(garbageRootNodeChildNames.contains(generateBasePath));
+        }
+
+        String generateBasePath = generatedBasePaths.get(0);
+        for (int i = 0; i < garbageNodesParentCount; i ++) {
+
+            for (int j = 0; j < nodesPerParent; j++) {
+
+                // check deleted node paths
+                List<String> deletedNodePaths = CreateGarbageCommand.generateGapOrphanNodePaths(
+                        generateBasePath, i, j, orphansDepth, orphansLevelGap, false);
+                for (String deletedNodePath : deletedNodePaths) {
+                    NodeDocument missingDocument = getDocument(cmd, Collection.NODES, deletedNodePath, 0);
+                    assertNull(missingDocument);
+                }
+
+                // check not deleted node paths - remaining gap orphans
+                List<String> notDeletedNodePaths = CreateGarbageCommand.generateGapOrphanNodePaths(generateBasePath, i, j, orphansDepth, orphansLevelGap, true);
+                for (String notDeletedNodePath : notDeletedNodePaths) {
+                    NodeDocument gapOrphanDocument = getDocument(cmd, Collection.NODES, notDeletedNodePath, 0);
+                    assertNotNull(gapOrphanDocument);
+                }
+            }
+        }
+    }
+
+    /**
+     * Utility method that returns the root node of the full gc generated garbage nodes.
+     * @param nodeStore
+     * @return
+     */
+    private static @NotNull NodeState getGarbageRootNode(DocumentNodeStore nodeStore) {
+        NodeState garbageRootNodeState = nodeStore.getRoot().getChildNode(CreateGarbageCommand.GARBAGE_GEN_ROOT_PATH_BASE).
+                getChildNode(CreateGarbageCommand.GARBAGE_GEN_ROOT_NODE_NAME);
+        return garbageRootNodeState;
+    }
+
+    /**
+     * Used in unit tests for retrieving a document by path from the document store using the documentNodeStore
+     * of this class.
+     * @param collection
+     * @param key
+     * @param maxCacheAge
+     * @return
+     * @param <T>
+     * @throws DocumentStoreException
+     */
+    @Nullable
+    private  <T extends Document> T getDocument(CreateGarbageCmd cmd, Collection<T> collection, String key, int maxCacheAge)
+            throws DocumentStoreException {
+        return cmd.getCommand().getDocumentNodeStore().getDocumentStore().find(collection, key, maxCacheAge);
+    }
+
+    /**
+     * Wrapper class for the GenerateGarbageCommand to be used in unit tests.
+     */
+    private static class CreateGarbageCmd implements Runnable {
+
+        private final ImmutableList<String> args;
+        private CreateGarbageCommand command;
+        private Closer closer;
+        private List<String> generatedBasePaths;
+
+        public CreateGarbageCmd(String... args) {
+            // append the default mongodb connection string if one was not provided
+            if (args[0].startsWith("mongodb://")) {
+                this.args = ImmutableList.copyOf(args);
+            } else {
+                this.args = ImmutableList.<String>builder().add(MongoUtils.URL)

Review Comment:
   Done, thank you for the suggestion.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "shodaaan (via GitHub)" <gi...@apache.org>.
shodaaan commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1809014806


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateFullGCCommand.java:
##########
@@ -0,0 +1,416 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.plugins.document.Collection;
+import org.apache.jackrabbit.oak.plugins.document.Document;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.DocumentStoreException;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateFullGCCommand implements Command {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateFullGCCommand.class);
+
+    private static final String USAGE = "generateFullGC {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String FULLGC_GEN_ROOT_PATH = "fullGCGenRoot";
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String FULLGC_GEN_BASE_PATH = "fullGCGenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String FULLGC_GEN_PARENT_NODE_PREFIX = "fullGCParent_";
+    public static String FULLGC_GEN_NODE_PREFIX = "fullGCNode_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node FULLGC_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public GenerateFullGCOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public GenerateFullGCOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {
+        Closer closer = Closer.create();
+        try {
+            execute(closer, args);
+        } catch (Throwable e) {
+            LOG.error("Command failed", e);
+            throw closer.rethrow(e);
+        } finally {
+            closer.close();
+        }
+    }
+
+    /**
+     * Method with passed closer is necessary in order to allow for unit tests to check the output of the command.
+     * It is the responsibility of the caller to close the closer.
+     *
+     * Returns the list of generated garbage base paths (under the garbage root node).
+     * @param closer
+     * @param args
+     * @throws Exception
+     */
+    public List<String> execute(Closer closer, String... args) throws Exception {
+        continuousRunIndex = 0;
+
+        List<String> generateBasePaths = new ArrayList<>();
+
+        GenerateFullGCOptions options = new GenerateFullGCOptions(USAGE).parse(args);
+        String subCmd = options.getSubCmd();
+
+        if (GenerateFullGCOptions.CMD_GENERATE.equals(subCmd)) {
+            if (options.getNumberOfRuns() > 1 && options.getGenerateIntervalSeconds() > 0) {
+                generateBasePaths.addAll(generateGarbageContinuously(options, closer));
+            } else {
+                generateBasePaths.add(generateGarbage(options, closer, 0));
+            }
+        } else if (GenerateFullGCOptions.CMD_CLEAN.equals(subCmd)) {
+            cleanGarbage(options, closer);
+        } else {
+            System.err.println("unknown revisions command: " + subCmd);
+        }
+
+        return generateBasePaths;
+    }
+
+    private List<String> generateGarbageContinuously(GenerateFullGCOptions options, Closer closer) throws IOException, Exception {
+        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long startGenTimestamp = System.currentTimeMillis();
+
+        List<String> generatedGarbageBasePaths = new ArrayList<>();
+
+        int numberOfRuns = options.getNumberOfRuns();
+        int intervalSeconds = options.getGenerateIntervalSeconds();
+        Runnable task = () -> {
+            if (continuousRunIndex < numberOfRuns) {
+                try {
+                    String genBasePath = generateGarbage(options, closer, continuousRunIndex, builder, startGenTimestamp);
+                    generatedGarbageBasePaths.add(genBasePath);
+                } catch (Exception e) {
+                    LOG.error("Error generating garbage in run " + continuousRunIndex, e);
+                }
+                LOG.info("Task executed. Count: " + (continuousRunIndex + 1));
+                continuousRunIndex++;
+            } else {
+                // Shutdown the executor once the task has run numberOfRuns times
+                executor.shutdown();
+                LOG.info("Task completed " + numberOfRuns + " times. Stopping execution.");
+            }
+        };
+
+        // Schedule the task to run every intervalSeconds
+        executor.scheduleAtFixedRate(task, 0, intervalSeconds, TimeUnit.SECONDS);
+
+        return generatedGarbageBasePaths;
+    }
+
+    /**
+     * Generate garbage nodes in the repository in order to allow for testing fullGC functionality.
+     *
+     * Returns the path of the generated FULLGC_GEN_BASE_PATH node (under the root).
+     * @param options
+     * @param closer
+     * @param runIndex
+     * @return
+     * @throws IOException
+     * @throws Exception
+     */
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long generationTimestamp = System.currentTimeMillis();
+
+        return generateGarbage(options, closer, runIndex, builder, generationTimestamp);
+    }
+
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex,
+                                  DocumentNodeStoreBuilder<?> builder, long timestamp) throws IOException, Exception {
+
+        if (builder == null) {
+            System.err.println("generateFullGC mode only available for DocumentNodeStore");
+            System.exit(1);
+        }
+
+        String generationBasePath = FULLGC_GEN_BASE_PATH + timestamp + "_" + runIndex;
+        System.out.println("Generating fullGC on the document: " + generationBasePath);
+        documentNodeStore = builder.build();
+
+        VersionGarbageCollector.FullGCMode fullGCMode = getFullGCMode(options);
+        if (fullGCMode == VersionGarbageCollector.FullGCMode.NONE) {
+            LOG.error("Invalid garbageType specified. Must be one of the following: 1 - EMPTYPROPS, 2 - GAP_ORPHANS, 3 - GAP_ORPHANS_EMPTYPROPS");
+            System.exit(1);
+        }
+
+        //1. Create nodes with properties
+        NodeBuilder rootNode = documentNodeStore.getRoot().builder();
+        NodeBuilder garbageRootNode = rootNode.child(FULLGC_GEN_ROOT_PATH);
+        garbageRootNode.child(generationBasePath).setProperty("jcr:primaryType", "nt:file", NAME);
+
+        int nodesCountUnderParent = options.getCreateGarbageNodesCount() / options.getGarbageNodesParentCount();

Review Comment:
   Added check for garbage nodes count and parent nodes count from options. Thank you for the suggestion.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "shodaaan (via GitHub)" <gi...@apache.org>.
shodaaan commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1838420792


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/plugins/document/CreateGarbageHelper.java:
##########
@@ -0,0 +1,37 @@
+/*
+ * 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;
+
+/**
+ * Helper class to generate garbage for testing purposes.
+ */
+public class CreateGarbageHelper {
+
+    public static boolean isInvalidGarbageGenerationMode(int fullGCMode) {
+        return fullGCMode == 0;
+    }
+
+    public static boolean isEmptyProps(int fullGCMode) {
+        return fullGCMode == 1;
+    }
+
+    public static boolean isGapOrphans(int fullGCMode) {
+        return fullGCMode == 2;
+    }

Review Comment:
   Fixed, thank you for the comment.



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/CreateGarbageCommand.java:
##########
@@ -0,0 +1,568 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.base.Joiner;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.CreateGarbageHelper;
+import org.apache.jackrabbit.oak.plugins.document.LeaseCheckMode;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.asCloseable;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * CreateGarbageCommand creates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class CreateGarbageCommand implements Command {
+    private static final Logger LOG = LoggerFactory.getLogger(CreateGarbageCommand.class);
+
+    private final ScheduledExecutorService continuousRunExecutor = Executors.newScheduledThreadPool(1);
+
+    private static final String USAGE = Joiner.on(System.lineSeparator()).join(
+            "create-test-garbage {<jdbc-uri> | <mongodb-uri>} <sub-command> [options] ",
+            "where sub-command is one of: ",
+            "  clean - clean up all generated garbage under the specified root node",
+            "  create - create garbage nodes in the repository, under the root node tmp/oak-run-generated-test-garbage. ",
+            "           Use the --garbageType (required) option to specify the type of garbage to create",
+            "           the --garbageNodesCount option to specify the total number of garbage nodes to create,",
+            "           the --garbageNodesParentCount option to specify the total number of parent nodes under which to create garbage nodes,",
+            "           the --orphansDepth option to specify the depth in the tree at which to create garbage nodes,",
+            "           the --orphansLevelGap option to specify the gap in the tree between the first leaf gap orphan garbage node and its parent,",
+            "           the --numberOfRuns option to specify the number of garbage generation runs to do,",
+            "           and the --generateIntervalSeconds option to specify the interval at which to generate a complete garbage count from createGarbageNotesCount.");
+
+
+    /**
+     * Garbage should be generated under tmp node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH_BASE = "tmp";
+
+    /**
+     * The root node name for garbage generation, one level under tmp.
+     */
+    public static String GARBAGE_GEN_ROOT_NODE_NAME = "oak-run-generated-test-garbage";
+
+    /**
+     * Root node for garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH = GARBAGE_GEN_ROOT_PATH_BASE + "/" + GARBAGE_GEN_ROOT_NODE_NAME;
+
+    /**
+     * Base path for garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String GEN_BASE_PATH = "GenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String GEN_PARENT_NODE_PREFIX = "Parent_";
+    public static String GEN_NODE_PREFIX = "Node_";
+    public static String GEN_NODE_LEVEL_PREFIX = "_Level_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    /**
+     * The maximum depth in the tree at which to generate gap orphans garbage nodes.
+     * Used for validation of the orphansDepth / orphansLevelGap options.
+     */
+    private final int GAP_ORPHANS_MAX_DEPTH = 15;
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class CreateGarbageOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_CREATE = "create";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node GARBAGE_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> orphansDepth;
+        final OptionSpec<Integer> orphansLevelGap;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public CreateGarbageOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            orphansDepth = parser
+                    .accepts("orphansDepth", "the depth in the tree at which to create garbage nodes. Only applies to ORPHANS fullGC modes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            orphansLevelGap = parser
+                    .accepts("orphansLevelGap", "the gap in the tree between the first leaf gap orphan garbage node and its' parent. Only applies to ORPHANS fullGC modes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public CreateGarbageOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_CREATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getOrphansDepth() { return orphansDepth.value(options);}
+
+        public int getOrphansLevelGap() { return orphansLevelGap.value(options); }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    @Override
+    public void execute(String... args) throws Exception {
+
+        try (Closer closer = Closer.create()) {
+            // Register the ExecutorService with Closer
+            closer.register(() -> {
+                // Properly shutdown the executor service
+                continuousRunExecutor.shutdown();
+                try {
+                    if (!continuousRunExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
+                        continuousRunExecutor.shutdownNow();
+                    }
+                } catch (InterruptedException e) {
+                    throw new RuntimeException(e);
+                }
+            });
+
+            execute(closer, args);
+
+        } catch (Throwable e) {
+            LOG.error("Command failed", e);
+            throw e;
+        }
+    }
+
+    /**
+     * Method with passed closer is necessary in order to allow for unit tests to check the output of the command.
+     * It is the responsibility of the caller to close the closer.
+     *
+     * Returns the list of generated garbage base paths (under the garbage root node).
+     * @param closer
+     * @param args
+     * @throws Exception
+     */
+    public List<String> execute(Closer closer, String... args) throws Exception {
+        continuousRunIndex = 0;
+
+        List<String> generateBasePaths = new ArrayList<>();
+
+        CreateGarbageOptions options = new CreateGarbageOptions(USAGE).parse(args);
+        String subCmd = options.getSubCmd();
+
+        LOG.info("Executing command: " + getClass().getName() + " with sub-command: " + subCmd);
+
+        if (CreateGarbageOptions.CMD_CREATE.equals(subCmd)) {
+
+            if (CreateGarbageHelper.isInvalidGarbageGenerationMode(options.getGarbageType())) {
+                LOG.error("Invalid fullGCMode specified: " + options.getGarbageType() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+            if (options.getCreateGarbageNodesCount() <= 0) {
+                LOG.error("Invalid garbageNodesCount specified: " + options.getCreateGarbageNodesCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+            if (options.getGarbageNodesParentCount() <= 0) {
+                LOG.error("Invalid garbageNodesParentCount specified: " + options.getGarbageNodesParentCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+
+            if (options.getNumberOfRuns() > 1 && options.getGenerateIntervalSeconds() > 0) {
+                generateBasePaths.addAll(generateGarbageContinuously(options, closer));
+            } else {
+                generateBasePaths.add(createGarbage(options, closer, 0));
+            }
+        } else if (CreateGarbageOptions.CMD_CLEAN.equals(subCmd)) {
+            cleanGarbage(options, closer);
+        } else {
+            System.err.println("Unknown revisions command: " + subCmd);
+        }
+
+        return generateBasePaths;
+    }
+
+    private List<String> generateGarbageContinuously(CreateGarbageOptions options, Closer closer) throws Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long startGenTimestamp = System.currentTimeMillis();
+
+        List<String> generatedGarbageBasePaths = new ArrayList<>();
+
+        int numberOfRuns = options.getNumberOfRuns();
+        int intervalSeconds = options.getGenerateIntervalSeconds();
+        Runnable task = () -> {
+            if (continuousRunIndex < numberOfRuns) {
+                try {
+                    String genBasePath = createGarbage(options, closer, continuousRunIndex, builder, startGenTimestamp);
+                    generatedGarbageBasePaths.add(genBasePath);
+                } catch (Exception e) {
+                    LOG.error("Error generating garbage in run " + continuousRunIndex, e);
+                }
+                LOG.info("Task executed. Count: " + (continuousRunIndex + 1));
+                continuousRunIndex++;
+            } else {
+                // Shutdown the executor once the task has run numberOfRuns times
+                continuousRunExecutor.shutdown();
+                LOG.info("Task completed " + numberOfRuns + " times. Stopping execution.");
+            }
+        };
+
+        // Schedule the task to run every intervalSeconds
+        continuousRunExecutor.scheduleAtFixedRate(task, 0, intervalSeconds, TimeUnit.SECONDS);
+
+        return generatedGarbageBasePaths;
+    }
+
+    /**
+     * Generate garbage nodes in the repository in order to allow for testing fullGC functionality.
+     *
+     * Returns the path of the generated GARBAGE_GEN_ROOT_PATH node (under the root).
+     * @param options
+     * @param closer
+     * @param runIndex
+     * @return
+     * @throws IOException
+     * @throws Exception
+     */
+    private String createGarbage(CreateGarbageOptions options, Closer closer, int runIndex) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long generationTimestamp = System.currentTimeMillis();
+
+        return createGarbage(options, closer, runIndex, builder, generationTimestamp);
+    }
+
+    private String createGarbage(CreateGarbageOptions options, Closer closer, int runIndex,
+                                 DocumentNodeStoreBuilder<?> builder, long timestamp) throws Exception {
+
+        if (builder == null) {
+            System.err.println("generateGarbage mode only available for DocumentNodeStore");
+            System.exit(1);
+        }
+
+        String generationBasePath = GEN_BASE_PATH + timestamp + "_" + runIndex;
+        documentNodeStore = builder.build();
+        closer.register(asCloseable(documentNodeStore));
+
+        NodeBuilder rootNode = documentNodeStore.getRoot().builder();

Review Comment:
   Fixed, thank you for the comment.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "shodaaan (via GitHub)" <gi...@apache.org>.
shodaaan commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1812630677


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateFullGCCommand.java:
##########
@@ -0,0 +1,416 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.plugins.document.Collection;
+import org.apache.jackrabbit.oak.plugins.document.Document;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.DocumentStoreException;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateFullGCCommand implements Command {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateFullGCCommand.class);
+
+    private static final String USAGE = "generateFullGC {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String FULLGC_GEN_ROOT_PATH = "fullGCGenRoot";
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String FULLGC_GEN_BASE_PATH = "fullGCGenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String FULLGC_GEN_PARENT_NODE_PREFIX = "fullGCParent_";
+    public static String FULLGC_GEN_NODE_PREFIX = "fullGCNode_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node FULLGC_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public GenerateFullGCOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public GenerateFullGCOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {
+        Closer closer = Closer.create();
+        try {
+            execute(closer, args);
+        } catch (Throwable e) {
+            LOG.error("Command failed", e);
+            throw closer.rethrow(e);
+        } finally {
+            closer.close();
+        }
+    }
+
+    /**
+     * Method with passed closer is necessary in order to allow for unit tests to check the output of the command.
+     * It is the responsibility of the caller to close the closer.
+     *
+     * Returns the list of generated garbage base paths (under the garbage root node).
+     * @param closer
+     * @param args
+     * @throws Exception
+     */
+    public List<String> execute(Closer closer, String... args) throws Exception {
+        continuousRunIndex = 0;
+
+        List<String> generateBasePaths = new ArrayList<>();
+
+        GenerateFullGCOptions options = new GenerateFullGCOptions(USAGE).parse(args);
+        String subCmd = options.getSubCmd();
+
+        if (GenerateFullGCOptions.CMD_GENERATE.equals(subCmd)) {
+            if (options.getNumberOfRuns() > 1 && options.getGenerateIntervalSeconds() > 0) {
+                generateBasePaths.addAll(generateGarbageContinuously(options, closer));
+            } else {
+                generateBasePaths.add(generateGarbage(options, closer, 0));
+            }
+        } else if (GenerateFullGCOptions.CMD_CLEAN.equals(subCmd)) {
+            cleanGarbage(options, closer);
+        } else {
+            System.err.println("unknown revisions command: " + subCmd);
+        }
+
+        return generateBasePaths;
+    }
+
+    private List<String> generateGarbageContinuously(GenerateFullGCOptions options, Closer closer) throws IOException, Exception {
+        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long startGenTimestamp = System.currentTimeMillis();
+
+        List<String> generatedGarbageBasePaths = new ArrayList<>();
+
+        int numberOfRuns = options.getNumberOfRuns();
+        int intervalSeconds = options.getGenerateIntervalSeconds();
+        Runnable task = () -> {
+            if (continuousRunIndex < numberOfRuns) {
+                try {
+                    String genBasePath = generateGarbage(options, closer, continuousRunIndex, builder, startGenTimestamp);
+                    generatedGarbageBasePaths.add(genBasePath);
+                } catch (Exception e) {
+                    LOG.error("Error generating garbage in run " + continuousRunIndex, e);
+                }
+                LOG.info("Task executed. Count: " + (continuousRunIndex + 1));
+                continuousRunIndex++;
+            } else {
+                // Shutdown the executor once the task has run numberOfRuns times
+                executor.shutdown();
+                LOG.info("Task completed " + numberOfRuns + " times. Stopping execution.");
+            }
+        };
+
+        // Schedule the task to run every intervalSeconds
+        executor.scheduleAtFixedRate(task, 0, intervalSeconds, TimeUnit.SECONDS);
+
+        return generatedGarbageBasePaths;
+    }
+
+    /**
+     * Generate garbage nodes in the repository in order to allow for testing fullGC functionality.
+     *
+     * Returns the path of the generated FULLGC_GEN_BASE_PATH node (under the root).
+     * @param options
+     * @param closer
+     * @param runIndex
+     * @return
+     * @throws IOException
+     * @throws Exception
+     */
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long generationTimestamp = System.currentTimeMillis();
+
+        return generateGarbage(options, closer, runIndex, builder, generationTimestamp);
+    }
+
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex,
+                                  DocumentNodeStoreBuilder<?> builder, long timestamp) throws IOException, Exception {
+
+        if (builder == null) {
+            System.err.println("generateFullGC mode only available for DocumentNodeStore");
+            System.exit(1);
+        }
+
+        String generationBasePath = FULLGC_GEN_BASE_PATH + timestamp + "_" + runIndex;
+        System.out.println("Generating fullGC on the document: " + generationBasePath);
+        documentNodeStore = builder.build();
+
+        VersionGarbageCollector.FullGCMode fullGCMode = getFullGCMode(options);
+        if (fullGCMode == VersionGarbageCollector.FullGCMode.NONE) {
+            LOG.error("Invalid garbageType specified. Must be one of the following: 1 - EMPTYPROPS, 2 - GAP_ORPHANS, 3 - GAP_ORPHANS_EMPTYPROPS");
+            System.exit(1);
+        }
+
+        //1. Create nodes with properties
+        NodeBuilder rootNode = documentNodeStore.getRoot().builder();
+        NodeBuilder garbageRootNode = rootNode.child(FULLGC_GEN_ROOT_PATH);
+        garbageRootNode.child(generationBasePath).setProperty("jcr:primaryType", "nt:file", NAME);
+
+        int nodesCountUnderParent = options.getCreateGarbageNodesCount() / options.getGarbageNodesParentCount();
+        for(int i = 0; i < options.getGarbageNodesParentCount(); i ++) {
+            garbageRootNode.child(generationBasePath).child(FULLGC_GEN_PARENT_NODE_PREFIX + i).setProperty("jcr:primaryType", "nt:folder", NAME);
+
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                garbageRootNode.child(generationBasePath).child(FULLGC_GEN_PARENT_NODE_PREFIX + i).child(FULLGC_GEN_NODE_PREFIX + j).
+                        setProperty("jcr:primaryType", "nt:file", NAME);
+
+                if (fullGCMode == VersionGarbageCollector.FullGCMode.EMPTYPROPS || fullGCMode == VersionGarbageCollector.FullGCMode.GAP_ORPHANS_EMPTYPROPS) {
+                    garbageRootNode.child(generationBasePath).child(FULLGC_GEN_PARENT_NODE_PREFIX + i).child(FULLGC_GEN_NODE_PREFIX + j).

Review Comment:
   Implemented, thank you for the suggestion.
   Added orphansDepth and orphansLevelGap parameters for running the command, which allow for generating gap orphans up to 15 levels deep and also allows for deleting nodes in their hierarchy up to the specified orphansLevelGap level.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "shodaaan (via GitHub)" <gi...@apache.org>.
shodaaan commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1812633264


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateGarbageCommand.java:
##########
@@ -0,0 +1,427 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.commons.concurrent.ExecutorCloser;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.GenerateGarbageHelper;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateGarbageCommand implements Command, Closeable {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateGarbageCommand.class);
+
+    private final ScheduledExecutorService continuousRunExecutor = Executors.newScheduledThreadPool(1);
+
+    private static final String USAGE = "createGarbage {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * FullGC garbage should be generated under tmp node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH_BASE = "tmp";
+
+    /**
+     * The root node name for fullGC garbage generation, one level under tmp.
+     */
+    public static String GARBAGE_GEN_ROOT_NODE_NAME = "fullGC_GarbageRoot";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH = GARBAGE_GEN_ROOT_PATH_BASE + "/" + GARBAGE_GEN_ROOT_NODE_NAME;
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String GEN_BASE_PATH = "GenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String GEN_PARENT_NODE_PREFIX = "Parent_";
+    public static String GEN_NODE_PREFIX = "Node_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node FULLGC_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public GenerateFullGCOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser

Review Comment:
   In the interest of keeping the functionality as simple as possible, we can do this in a future update of the GenerateGarbageCommand, if necessary.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "shodaaan (via GitHub)" <gi...@apache.org>.
shodaaan commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1838421152


##########
oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/VersionGarbageCollector.java:
##########
@@ -168,7 +168,7 @@ public class VersionGarbageCollector {
      * Ultimately the goal is to clean up all possible garbage. After hardening these modes
      * might no longer be supported.
      */
-    enum FullGCMode {
+     enum FullGCMode {

Review Comment:
   Fixed, thank you for the comment.



##########
oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/FullGCHelper.java:
##########
@@ -70,6 +70,14 @@ public static RevisionVector unmergedBranchCommit(final NodeStore store, final C
         return result;
     }
 
+    public static boolean includesEmptyProps(int fullGCMode) {
+        return fullGCMode == 1;
+    }
+
+    public static boolean includesGapOrphans(int fullGCMode) {
+        return fullGCMode == 2;
+    }
+

Review Comment:
   Fixed, thank you for the comment.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "stefan-egli (via GitHub)" <gi...@apache.org>.
stefan-egli commented on PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#issuecomment-2476418530

   > would suggest to go ahead with merge unless we find a regression
   
   **squash merge** obviously!


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "shodaaan (via GitHub)" <gi...@apache.org>.
shodaaan commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1820293340


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateGarbageCommand.java:
##########
@@ -0,0 +1,553 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.commons.concurrent.ExecutorCloser;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.GenerateGarbageHelper;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateGarbageCommand implements Command, Closeable {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateGarbageCommand.class);
+
+    private final ScheduledExecutorService continuousRunExecutor = Executors.newScheduledThreadPool(1);
+
+    private static final String USAGE = "createGarbage {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * FullGC garbage should be generated under tmp node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH_BASE = "tmp";
+
+    /**
+     * The root node name for fullGC garbage generation, one level under tmp.
+     */
+    public static String GARBAGE_GEN_ROOT_NODE_NAME = "fullGC_GarbageRoot";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH = GARBAGE_GEN_ROOT_PATH_BASE + "/" + GARBAGE_GEN_ROOT_NODE_NAME;
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String GEN_BASE_PATH = "GenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String GEN_PARENT_NODE_PREFIX = "Parent_";
+    public static String GEN_NODE_PREFIX = "Node_";
+    public static String GEN_NODE_LEVEL_PREFIX = "_Level_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    /**
+     * The maximum depth in the tree at which to generate gap orphans garbage nodes.
+     * Used for validation of the orphansDepth / orphansLevelGap options.
+     */
+    private final int GAP_ORPHANS_MAX_DEPTH = 15;
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {

Review Comment:
   Changed, thank you for the suggestion.



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateGarbageCommand.java:
##########
@@ -0,0 +1,553 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.commons.concurrent.ExecutorCloser;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.GenerateGarbageHelper;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateGarbageCommand implements Command, Closeable {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateGarbageCommand.class);
+
+    private final ScheduledExecutorService continuousRunExecutor = Executors.newScheduledThreadPool(1);
+
+    private static final String USAGE = "createGarbage {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * FullGC garbage should be generated under tmp node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH_BASE = "tmp";
+
+    /**
+     * The root node name for fullGC garbage generation, one level under tmp.
+     */
+    public static String GARBAGE_GEN_ROOT_NODE_NAME = "fullGC_GarbageRoot";

Review Comment:
   Changed, thank you for the suggestion.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "shodaaan (via GitHub)" <gi...@apache.org>.
shodaaan commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1820536936


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/AvailableModes.java:
##########
@@ -75,5 +75,6 @@ public final class AvailableModes {
             .put("segment-copy", new SegmentCopyCommand())
             .put("server", new ServerCommand())
             .put("purge-index-versions", new LucenePurgeOldIndexVersionCommand())
+            .put("generate-test-garbage", new GenerateGarbageCommand())

Review Comment:
   Fixed, thank you for the suggestion.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "stefan-egli (via GitHub)" <gi...@apache.org>.
stefan-egli commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1820610735


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateGarbageCommand.java:
##########
@@ -0,0 +1,553 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.commons.concurrent.ExecutorCloser;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.GenerateGarbageHelper;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateGarbageCommand implements Command, Closeable {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateGarbageCommand.class);
+
+    private final ScheduledExecutorService continuousRunExecutor = Executors.newScheduledThreadPool(1);
+
+    private static final String USAGE = "createGarbage {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * FullGC garbage should be generated under tmp node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH_BASE = "tmp";
+
+    /**
+     * The root node name for fullGC garbage generation, one level under tmp.
+     */
+    public static String GARBAGE_GEN_ROOT_NODE_NAME = "fullGC_GarbageRoot";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH = GARBAGE_GEN_ROOT_PATH_BASE + "/" + GARBAGE_GEN_ROOT_NODE_NAME;
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String GEN_BASE_PATH = "GenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String GEN_PARENT_NODE_PREFIX = "Parent_";
+    public static String GEN_NODE_PREFIX = "Node_";
+    public static String GEN_NODE_LEVEL_PREFIX = "_Level_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    /**
+     * The maximum depth in the tree at which to generate gap orphans garbage nodes.
+     * Used for validation of the orphansDepth / orphansLevelGap options.
+     */
+    private final int GAP_ORPHANS_MAX_DEPTH = 15;
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node FULLGC_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> orphansDepth;
+        final OptionSpec<Integer> orphansLevelGap;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public GenerateFullGCOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            orphansDepth = parser
+                    .accepts("orphansDepth", "the depth in the tree at which to create garbage nodes. Only applies to ORPHANS fullGC modes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            orphansLevelGap = parser
+                    .accepts("orphansLevelGap", "the gap in the tree between the first leaf gap orphan garbage node and its' parent. Only applies to ORPHANS fullGC modes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public GenerateFullGCOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getOrphansDepth() { return orphansDepth.value(options);}
+
+        public int getOrphansLevelGap() { return orphansLevelGap.value(options); }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {
+
+        try (Closer closer = Closer.create()) {
+            // Register the ExecutorService with Closer
+            closer.register(() -> {
+                // Properly shutdown the executor service
+                continuousRunExecutor.shutdown();
+                try {
+                    if (!continuousRunExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
+                        continuousRunExecutor.shutdownNow();
+                    }
+                } catch (InterruptedException e) {
+                    throw new RuntimeException(e);
+                }
+            });
+            execute(closer, args);
+        } catch (Throwable e) {
+            LOG.error("Command failed", e);
+            throw e;
+        }
+    }
+
+    /**
+     * Method with passed closer is necessary in order to allow for unit tests to check the output of the command.
+     * It is the responsibility of the caller to close the closer.
+     *
+     * Returns the list of generated garbage base paths (under the garbage root node).
+     * @param closer
+     * @param args
+     * @throws Exception
+     */
+    public List<String> execute(Closer closer, String... args) throws Exception {
+        continuousRunIndex = 0;
+
+        List<String> generateBasePaths = new ArrayList<>();
+
+        GenerateFullGCOptions options = new GenerateFullGCOptions(USAGE).parse(args);
+        String subCmd = options.getSubCmd();
+
+        if (GenerateFullGCOptions.CMD_GENERATE.equals(subCmd)) {
+
+            if (options.getCreateGarbageNodesCount() <= 0) {
+                LOG.error("Invalid garbageNodesCount specified: " + options.getCreateGarbageNodesCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+            if (options.getGarbageNodesParentCount() <= 0) {
+                LOG.error("Invalid garbageNodesParentCount specified: " + options.getGarbageNodesParentCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+
+            if (options.getNumberOfRuns() > 1 && options.getGenerateIntervalSeconds() > 0) {
+                generateBasePaths.addAll(generateGarbageContinuously(options, closer));
+            } else {
+                generateBasePaths.add(generateGarbage(options, closer, 0));
+            }
+        } else if (GenerateFullGCOptions.CMD_CLEAN.equals(subCmd)) {
+            cleanGarbage(options, closer);
+        } else {
+            System.err.println("Unknown revisions command: " + subCmd);
+        }
+
+        return generateBasePaths;
+    }
+
+    @Override
+    public void close() throws IOException {
+        new ExecutorCloser(this.continuousRunExecutor).close();
+    }
+
+    private List<String> generateGarbageContinuously(GenerateFullGCOptions options, Closer closer) throws Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long startGenTimestamp = System.currentTimeMillis();
+
+        List<String> generatedGarbageBasePaths = new ArrayList<>();
+
+        int numberOfRuns = options.getNumberOfRuns();
+        int intervalSeconds = options.getGenerateIntervalSeconds();
+        Runnable task = () -> {
+            if (continuousRunIndex < numberOfRuns) {
+                try {
+                    String genBasePath = generateGarbage(options, continuousRunIndex, builder, startGenTimestamp);
+                    generatedGarbageBasePaths.add(genBasePath);
+                } catch (Exception e) {
+                    LOG.error("Error generating garbage in run " + continuousRunIndex, e);
+                }
+                LOG.info("Task executed. Count: " + (continuousRunIndex + 1));
+                continuousRunIndex++;
+            } else {
+                // Shutdown the executor once the task has run numberOfRuns times
+                continuousRunExecutor.shutdown();
+                LOG.info("Task completed " + numberOfRuns + " times. Stopping execution.");
+            }
+        };
+
+        // Schedule the task to run every intervalSeconds
+        continuousRunExecutor.scheduleAtFixedRate(task, 0, intervalSeconds, TimeUnit.SECONDS);
+
+        return generatedGarbageBasePaths;
+    }
+
+    /**
+     * Generate garbage nodes in the repository in order to allow for testing fullGC functionality.
+     *
+     * Returns the path of the generated FULLGC_GEN_BASE_PATH node (under the root).
+     * @param options
+     * @param closer
+     * @param runIndex
+     * @return
+     * @throws IOException
+     * @throws Exception
+     */
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long generationTimestamp = System.currentTimeMillis();
+
+        return generateGarbage(options, runIndex, builder, generationTimestamp);
+    }
+
+    private String generateGarbage(GenerateFullGCOptions options, int runIndex,
+                                   DocumentNodeStoreBuilder<?> builder, long timestamp) throws Exception {
+
+        if (builder == null) {
+            System.err.println("generateFullGC mode only available for DocumentNodeStore");
+            System.exit(1);
+        }
+
+        if (GenerateGarbageHelper.isInvalidGarbageGenerationMode(options.getGarbageType())) {
+            LOG.error("Invalid fullGCMode specified: " + options.getGarbageType() + " in: " + getClass().getName());
+            System.exit(1);
+        }
+
+        String generationBasePath = GEN_BASE_PATH + timestamp + "_" + runIndex;
+        documentNodeStore = builder.build();
+
+        NodeBuilder rootNode = documentNodeStore.getRoot().builder();
+
+        if (GenerateGarbageHelper.isEmptyProps(options.getGarbageType())) {
+            generateGarbageEmptyProps(rootNode, options, generationBasePath);
+        }
+        else if (GenerateGarbageHelper.isGapOrphans(options.getGarbageType())) {
+            generateGarbageGapOrphans(rootNode, options, generationBasePath);
+        }
+
+        return generationBasePath;
+    }
+    private void generateGarbageGapOrphans(NodeBuilder rootNode, GenerateFullGCOptions options, String generationBasePath)
+            throws CommitFailedException {
+
+        // validate gap orphans depth and level gap
+        if (options.getOrphansDepth() < 0 || options.getOrphansDepth() > GAP_ORPHANS_MAX_DEPTH) {
+            LOG.error("Invalid orphansDepth specified: " + options.getOrphansDepth() + " in: " + getClass().getName()
+                    + ". Must be an integer >= 0 and <= " + GAP_ORPHANS_MAX_DEPTH);
+            System.exit(1);
+        }
+        if (options.getOrphansLevelGap() < 0 || options.getOrphansLevelGap() > options.getOrphansDepth()) {
+            LOG.error("Invalid orphansLevelGap specified: " + options.getOrphansLevelGap() + " in: " + getClass().getName()
+                    + ". Must be a positive integer <= orphansDepth");
+            System.exit(1);
+        }
+
+        System.out.println("Generating fullGC gap orphans on the document: " + generationBasePath);
+
+        NodeBuilder garbageRootNode = rootNode.child(GARBAGE_GEN_ROOT_PATH_BASE).child(GARBAGE_GEN_ROOT_NODE_NAME);
+        garbageRootNode.child(generationBasePath).setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+
+        int nodesCountUnderParent = options.getCreateGarbageNodesCount() / options.getGarbageNodesParentCount();
+        for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+            // create parent node
+            garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).setProperty(JcrConstants.JCR_PRIMARYTYPE, "nt:folder", NAME);
+
+            // create child nodes under parent, according to gap orphans depth
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                getGapOrphanLeafGarbageNode(garbageRootNode, generationBasePath, options.getOrphansDepth(), i, j).
+                        setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+            }
+        }
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+
+        // Generate garbage nodes - GAP_ORPHANS - remove parent nodes
+        StringBuilder sbNodePath = new StringBuilder();
+        List<String> deleteNodePaths = new ArrayList<>();
+        for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+
+            // append the parent to the paths to delete
+            sbNodePath.setLength(0);
+            String path = getIdFromPath(
+                    sbNodePath.append("/").append(GARBAGE_GEN_ROOT_PATH).append("/").append(generationBasePath).append("/")
+                            .append(GEN_PARENT_NODE_PREFIX).append(i).toString());
+            deleteNodePaths.add(path);
+
+            // append all the gap orphans nodes between the parent and the level gap to the paths to delete
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                deleteNodePaths.addAll(generateGapOrphanNodePaths(generationBasePath, i, j, options.getOrphansDepth(), options.getOrphansLevelGap(), false));
+            }
+        }
+        // Remove all parent nodes
+        documentNodeStore.getDocumentStore().remove(org.apache.jackrabbit.oak.plugins.document.Collection.NODES, deleteNodePaths);
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+    }
+
+    private NodeBuilder getGapOrphanLeafGarbageNode(NodeBuilder garbageRootNode, String generationBasePath, int depth, int parentIndex, int nodeIndex) {
+        NodeBuilder garbageNode = garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + parentIndex);
+        for (int i = 1; i < depth; i++) {
+            garbageNode = garbageNode.child(GEN_NODE_PREFIX + nodeIndex + GEN_NODE_LEVEL_PREFIX + i);
+        }
+        // the leaf garbage node does not have level in its name
+        garbageNode = garbageNode.child(GEN_NODE_PREFIX + nodeIndex);
+        return garbageNode;
+    }
+
+    public static List<String> generateGapOrphanNodePaths(String generationBasePath, int parentIndex, int nodeIndex, int gapDepth) {
+        return generateGapOrphanNodePaths(generationBasePath, parentIndex, nodeIndex, gapDepth, 0, false);
+    }
+
+    /**
+     * Gets the paths of the nodes between the parent node and the level gap OR between the level gap and the leaf
+     * gap orphan node.
+     *
+     * Example 1: gapDepth = 4, gapLevelGap = 2, fromLevelGapToLeaf = false will return:
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2
+     * and will NOT return the rest of the nodes:
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3/Node_0 (the leaf node)
+     *
+     * Example 2: gapDepth = 5, gapLevelGap = 2, fromLevelGapToLeaf = true will return:
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3/Node_0_Level_4
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3/Node_0_Level_4/Node_0 (the leaf node)
+     * and will NOT return the nodes from the parent to the gap level:
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2
+     *
+     * @param generationBasePath
+     * @param parentIndex
+     * @param nodeIndex
+     * @param gapDepth - the depth in the tree until which to create gap orphan garbage nodes.
+     * @param gapLevelGap - the gap in the tree between the first gap orphan garbage node and its parent.
+     * @param fromLevelGapToLeaf - if true, the paths will be generated from the level gap to the leaf node, otherwise the paths will be generated from the parent node to the level gap.
+     * @return
+     */
+    public static List<String> generateGapOrphanNodePaths(String generationBasePath, int parentIndex, int nodeIndex, int gapDepth, int gapLevelGap, boolean fromLevelGapToLeaf) {
+        List<String> gapOrphanNodePaths = new ArrayList<>();
+
+        StringBuilder sbNodePath = new StringBuilder();
+        sbNodePath.setLength(0);
+
+        // start path from the parent node
+        sbNodePath.append("/").append(GARBAGE_GEN_ROOT_PATH).append("/").append(generationBasePath).append("/")
+                .append(GEN_PARENT_NODE_PREFIX).append(parentIndex);
+
+        for(int i = 1; i <= gapDepth; i ++) {
+            sbNodePath.append("/").append(GEN_NODE_PREFIX).append(nodeIndex);
+            // the leaf node does NOT have level in its name
+            if (i < gapDepth) {
+                sbNodePath.append(GEN_NODE_LEVEL_PREFIX).append(i);
+            }
+
+            // only append to list if the criteria for returning the node path is met
+            if ((!fromLevelGapToLeaf && i < gapLevelGap) || (fromLevelGapToLeaf && i >= gapLevelGap)) {
+                gapOrphanNodePaths.add(getIdFromPath(sbNodePath.toString()));
+            }
+        }
+
+        return gapOrphanNodePaths;
+    }
+
+    private void generateGarbageEmptyProps(NodeBuilder rootNode, GenerateFullGCOptions options, String generationBasePath) throws CommitFailedException {
+
+        System.out.println("Generating fullGC empty props on the document: " + generationBasePath);
+
+        NodeBuilder garbageRootNode = rootNode.child(GARBAGE_GEN_ROOT_PATH_BASE).child(GARBAGE_GEN_ROOT_NODE_NAME);
+        garbageRootNode.child(generationBasePath).setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+
+        int nodesCountUnderParent = options.getCreateGarbageNodesCount() / options.getGarbageNodesParentCount();
+        for(int i = 0; i < options.getGarbageNodesParentCount(); i ++) {
+            garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).setProperty(JcrConstants.JCR_PRIMARYTYPE, "nt:folder", NAME);
+
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                        setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+
+                if (GenerateGarbageHelper.isEmptyProps(options.getGarbageType())) {
+                    garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                            setProperty(EMPTY_PROPERTY_NAME, "bar", STRING);
+                }
+            }
+        }
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+
+
+        //2. Generate garbage nodes - EMPTY_PROPERTIES
+        for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+            for (int j = 0; j < nodesCountUnderParent; j++) {
+                garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                        removeProperty(EMPTY_PROPERTY_NAME);
+            }
+        }
+
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+    }
+
+    /**
+     * Cleans up all generated garbage by removing the node FULLGC_GEN_ROOT_PATH and all of
+     * its children (recursively)
+     * @param options
+     * @param closer
+     * @throws IOException
+     * @throws Exception
+     */
+    private void cleanGarbage(GenerateFullGCOptions options, Closer closer) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+
+        if (builder == null) {
+            System.err.println("generateFullGC mode only available for DocumentNodeStore");
+            System.exit(1);
+        }
+
+        System.out.println("Cleaning up all generated garbage:");
+        documentNodeStore = builder.build();
+
+        NodeBuilder rootBuilder = documentNodeStore.getRoot().builder();
+
+        NodeBuilder generatedGarbageRootBuilder = rootBuilder.child(GARBAGE_GEN_ROOT_PATH_BASE).child(GARBAGE_GEN_ROOT_NODE_NAME);
+
+        String garbageRootNodePath = "2:/"+ GARBAGE_GEN_ROOT_PATH;
+        List<String> childNodePaths = new ArrayList<>();
+        childNodePaths.add(garbageRootNodePath);
+
+        // get all paths of the tree nodes under the garbage root node
+        getTreeNodePaths(generatedGarbageRootBuilder, garbageRootNodePath, childNodePaths, 2);
+
+        documentNodeStore.getDocumentStore().remove(org.apache.jackrabbit.oak.plugins.document.Collection.NODES, childNodePaths);
+        documentNodeStore.merge(rootBuilder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();

Review Comment:
   any particular reason you would like to keep these ? I think they apply to tests but not in this case.



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateGarbageCommand.java:
##########
@@ -0,0 +1,553 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.commons.concurrent.ExecutorCloser;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.GenerateGarbageHelper;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateGarbageCommand implements Command, Closeable {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateGarbageCommand.class);
+
+    private final ScheduledExecutorService continuousRunExecutor = Executors.newScheduledThreadPool(1);
+
+    private static final String USAGE = "createGarbage {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * FullGC garbage should be generated under tmp node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH_BASE = "tmp";
+
+    /**
+     * The root node name for fullGC garbage generation, one level under tmp.
+     */
+    public static String GARBAGE_GEN_ROOT_NODE_NAME = "fullGC_GarbageRoot";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH = GARBAGE_GEN_ROOT_PATH_BASE + "/" + GARBAGE_GEN_ROOT_NODE_NAME;
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String GEN_BASE_PATH = "GenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String GEN_PARENT_NODE_PREFIX = "Parent_";
+    public static String GEN_NODE_PREFIX = "Node_";
+    public static String GEN_NODE_LEVEL_PREFIX = "_Level_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    /**
+     * The maximum depth in the tree at which to generate gap orphans garbage nodes.
+     * Used for validation of the orphansDepth / orphansLevelGap options.
+     */
+    private final int GAP_ORPHANS_MAX_DEPTH = 15;
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node FULLGC_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> orphansDepth;
+        final OptionSpec<Integer> orphansLevelGap;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public GenerateFullGCOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            orphansDepth = parser
+                    .accepts("orphansDepth", "the depth in the tree at which to create garbage nodes. Only applies to ORPHANS fullGC modes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            orphansLevelGap = parser
+                    .accepts("orphansLevelGap", "the gap in the tree between the first leaf gap orphan garbage node and its' parent. Only applies to ORPHANS fullGC modes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public GenerateFullGCOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getOrphansDepth() { return orphansDepth.value(options);}
+
+        public int getOrphansLevelGap() { return orphansLevelGap.value(options); }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {
+
+        try (Closer closer = Closer.create()) {
+            // Register the ExecutorService with Closer
+            closer.register(() -> {
+                // Properly shutdown the executor service
+                continuousRunExecutor.shutdown();
+                try {
+                    if (!continuousRunExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
+                        continuousRunExecutor.shutdownNow();
+                    }
+                } catch (InterruptedException e) {
+                    throw new RuntimeException(e);
+                }
+            });
+            execute(closer, args);
+        } catch (Throwable e) {
+            LOG.error("Command failed", e);
+            throw e;
+        }
+    }
+
+    /**
+     * Method with passed closer is necessary in order to allow for unit tests to check the output of the command.
+     * It is the responsibility of the caller to close the closer.
+     *
+     * Returns the list of generated garbage base paths (under the garbage root node).
+     * @param closer
+     * @param args
+     * @throws Exception
+     */
+    public List<String> execute(Closer closer, String... args) throws Exception {
+        continuousRunIndex = 0;
+
+        List<String> generateBasePaths = new ArrayList<>();
+
+        GenerateFullGCOptions options = new GenerateFullGCOptions(USAGE).parse(args);
+        String subCmd = options.getSubCmd();
+
+        if (GenerateFullGCOptions.CMD_GENERATE.equals(subCmd)) {
+
+            if (options.getCreateGarbageNodesCount() <= 0) {
+                LOG.error("Invalid garbageNodesCount specified: " + options.getCreateGarbageNodesCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+            if (options.getGarbageNodesParentCount() <= 0) {
+                LOG.error("Invalid garbageNodesParentCount specified: " + options.getGarbageNodesParentCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+
+            if (options.getNumberOfRuns() > 1 && options.getGenerateIntervalSeconds() > 0) {
+                generateBasePaths.addAll(generateGarbageContinuously(options, closer));
+            } else {
+                generateBasePaths.add(generateGarbage(options, closer, 0));
+            }
+        } else if (GenerateFullGCOptions.CMD_CLEAN.equals(subCmd)) {
+            cleanGarbage(options, closer);
+        } else {
+            System.err.println("Unknown revisions command: " + subCmd);
+        }
+
+        return generateBasePaths;
+    }
+
+    @Override
+    public void close() throws IOException {
+        new ExecutorCloser(this.continuousRunExecutor).close();
+    }
+
+    private List<String> generateGarbageContinuously(GenerateFullGCOptions options, Closer closer) throws Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long startGenTimestamp = System.currentTimeMillis();
+
+        List<String> generatedGarbageBasePaths = new ArrayList<>();
+
+        int numberOfRuns = options.getNumberOfRuns();
+        int intervalSeconds = options.getGenerateIntervalSeconds();
+        Runnable task = () -> {
+            if (continuousRunIndex < numberOfRuns) {
+                try {
+                    String genBasePath = generateGarbage(options, continuousRunIndex, builder, startGenTimestamp);
+                    generatedGarbageBasePaths.add(genBasePath);
+                } catch (Exception e) {
+                    LOG.error("Error generating garbage in run " + continuousRunIndex, e);
+                }
+                LOG.info("Task executed. Count: " + (continuousRunIndex + 1));
+                continuousRunIndex++;
+            } else {
+                // Shutdown the executor once the task has run numberOfRuns times
+                continuousRunExecutor.shutdown();
+                LOG.info("Task completed " + numberOfRuns + " times. Stopping execution.");
+            }
+        };
+
+        // Schedule the task to run every intervalSeconds
+        continuousRunExecutor.scheduleAtFixedRate(task, 0, intervalSeconds, TimeUnit.SECONDS);
+
+        return generatedGarbageBasePaths;
+    }
+
+    /**
+     * Generate garbage nodes in the repository in order to allow for testing fullGC functionality.
+     *
+     * Returns the path of the generated FULLGC_GEN_BASE_PATH node (under the root).
+     * @param options
+     * @param closer
+     * @param runIndex
+     * @return
+     * @throws IOException
+     * @throws Exception
+     */
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long generationTimestamp = System.currentTimeMillis();
+
+        return generateGarbage(options, runIndex, builder, generationTimestamp);
+    }
+
+    private String generateGarbage(GenerateFullGCOptions options, int runIndex,
+                                   DocumentNodeStoreBuilder<?> builder, long timestamp) throws Exception {
+
+        if (builder == null) {
+            System.err.println("generateFullGC mode only available for DocumentNodeStore");
+            System.exit(1);
+        }
+
+        if (GenerateGarbageHelper.isInvalidGarbageGenerationMode(options.getGarbageType())) {
+            LOG.error("Invalid fullGCMode specified: " + options.getGarbageType() + " in: " + getClass().getName());
+            System.exit(1);
+        }
+
+        String generationBasePath = GEN_BASE_PATH + timestamp + "_" + runIndex;
+        documentNodeStore = builder.build();
+
+        NodeBuilder rootNode = documentNodeStore.getRoot().builder();
+
+        if (GenerateGarbageHelper.isEmptyProps(options.getGarbageType())) {
+            generateGarbageEmptyProps(rootNode, options, generationBasePath);
+        }
+        else if (GenerateGarbageHelper.isGapOrphans(options.getGarbageType())) {
+            generateGarbageGapOrphans(rootNode, options, generationBasePath);
+        }
+
+        return generationBasePath;
+    }
+    private void generateGarbageGapOrphans(NodeBuilder rootNode, GenerateFullGCOptions options, String generationBasePath)
+            throws CommitFailedException {
+
+        // validate gap orphans depth and level gap
+        if (options.getOrphansDepth() < 0 || options.getOrphansDepth() > GAP_ORPHANS_MAX_DEPTH) {
+            LOG.error("Invalid orphansDepth specified: " + options.getOrphansDepth() + " in: " + getClass().getName()
+                    + ". Must be an integer >= 0 and <= " + GAP_ORPHANS_MAX_DEPTH);
+            System.exit(1);
+        }
+        if (options.getOrphansLevelGap() < 0 || options.getOrphansLevelGap() > options.getOrphansDepth()) {
+            LOG.error("Invalid orphansLevelGap specified: " + options.getOrphansLevelGap() + " in: " + getClass().getName()
+                    + ". Must be a positive integer <= orphansDepth");
+            System.exit(1);
+        }
+
+        System.out.println("Generating fullGC gap orphans on the document: " + generationBasePath);
+
+        NodeBuilder garbageRootNode = rootNode.child(GARBAGE_GEN_ROOT_PATH_BASE).child(GARBAGE_GEN_ROOT_NODE_NAME);
+        garbageRootNode.child(generationBasePath).setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+
+        int nodesCountUnderParent = options.getCreateGarbageNodesCount() / options.getGarbageNodesParentCount();
+        for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+            // create parent node
+            garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).setProperty(JcrConstants.JCR_PRIMARYTYPE, "nt:folder", NAME);
+
+            // create child nodes under parent, according to gap orphans depth
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                getGapOrphanLeafGarbageNode(garbageRootNode, generationBasePath, options.getOrphansDepth(), i, j).
+                        setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+            }
+        }
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+
+        // Generate garbage nodes - GAP_ORPHANS - remove parent nodes
+        StringBuilder sbNodePath = new StringBuilder();
+        List<String> deleteNodePaths = new ArrayList<>();
+        for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+
+            // append the parent to the paths to delete
+            sbNodePath.setLength(0);
+            String path = getIdFromPath(
+                    sbNodePath.append("/").append(GARBAGE_GEN_ROOT_PATH).append("/").append(generationBasePath).append("/")
+                            .append(GEN_PARENT_NODE_PREFIX).append(i).toString());
+            deleteNodePaths.add(path);
+
+            // append all the gap orphans nodes between the parent and the level gap to the paths to delete
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                deleteNodePaths.addAll(generateGapOrphanNodePaths(generationBasePath, i, j, options.getOrphansDepth(), options.getOrphansLevelGap(), false));
+            }
+        }
+        // Remove all parent nodes
+        documentNodeStore.getDocumentStore().remove(org.apache.jackrabbit.oak.plugins.document.Collection.NODES, deleteNodePaths);
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+    }
+
+    private NodeBuilder getGapOrphanLeafGarbageNode(NodeBuilder garbageRootNode, String generationBasePath, int depth, int parentIndex, int nodeIndex) {
+        NodeBuilder garbageNode = garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + parentIndex);
+        for (int i = 1; i < depth; i++) {
+            garbageNode = garbageNode.child(GEN_NODE_PREFIX + nodeIndex + GEN_NODE_LEVEL_PREFIX + i);
+        }
+        // the leaf garbage node does not have level in its name
+        garbageNode = garbageNode.child(GEN_NODE_PREFIX + nodeIndex);
+        return garbageNode;
+    }
+
+    public static List<String> generateGapOrphanNodePaths(String generationBasePath, int parentIndex, int nodeIndex, int gapDepth) {
+        return generateGapOrphanNodePaths(generationBasePath, parentIndex, nodeIndex, gapDepth, 0, false);
+    }
+
+    /**
+     * Gets the paths of the nodes between the parent node and the level gap OR between the level gap and the leaf
+     * gap orphan node.
+     *
+     * Example 1: gapDepth = 4, gapLevelGap = 2, fromLevelGapToLeaf = false will return:
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2
+     * and will NOT return the rest of the nodes:
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3/Node_0 (the leaf node)
+     *
+     * Example 2: gapDepth = 5, gapLevelGap = 2, fromLevelGapToLeaf = true will return:
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3/Node_0_Level_4
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3/Node_0_Level_4/Node_0 (the leaf node)
+     * and will NOT return the nodes from the parent to the gap level:
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2
+     *
+     * @param generationBasePath
+     * @param parentIndex
+     * @param nodeIndex
+     * @param gapDepth - the depth in the tree until which to create gap orphan garbage nodes.
+     * @param gapLevelGap - the gap in the tree between the first gap orphan garbage node and its parent.
+     * @param fromLevelGapToLeaf - if true, the paths will be generated from the level gap to the leaf node, otherwise the paths will be generated from the parent node to the level gap.
+     * @return
+     */
+    public static List<String> generateGapOrphanNodePaths(String generationBasePath, int parentIndex, int nodeIndex, int gapDepth, int gapLevelGap, boolean fromLevelGapToLeaf) {
+        List<String> gapOrphanNodePaths = new ArrayList<>();
+
+        StringBuilder sbNodePath = new StringBuilder();
+        sbNodePath.setLength(0);
+
+        // start path from the parent node
+        sbNodePath.append("/").append(GARBAGE_GEN_ROOT_PATH).append("/").append(generationBasePath).append("/")
+                .append(GEN_PARENT_NODE_PREFIX).append(parentIndex);
+
+        for(int i = 1; i <= gapDepth; i ++) {
+            sbNodePath.append("/").append(GEN_NODE_PREFIX).append(nodeIndex);
+            // the leaf node does NOT have level in its name
+            if (i < gapDepth) {
+                sbNodePath.append(GEN_NODE_LEVEL_PREFIX).append(i);
+            }
+
+            // only append to list if the criteria for returning the node path is met
+            if ((!fromLevelGapToLeaf && i < gapLevelGap) || (fromLevelGapToLeaf && i >= gapLevelGap)) {
+                gapOrphanNodePaths.add(getIdFromPath(sbNodePath.toString()));
+            }
+        }
+
+        return gapOrphanNodePaths;
+    }
+
+    private void generateGarbageEmptyProps(NodeBuilder rootNode, GenerateFullGCOptions options, String generationBasePath) throws CommitFailedException {
+
+        System.out.println("Generating fullGC empty props on the document: " + generationBasePath);
+
+        NodeBuilder garbageRootNode = rootNode.child(GARBAGE_GEN_ROOT_PATH_BASE).child(GARBAGE_GEN_ROOT_NODE_NAME);
+        garbageRootNode.child(generationBasePath).setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+
+        int nodesCountUnderParent = options.getCreateGarbageNodesCount() / options.getGarbageNodesParentCount();
+        for(int i = 0; i < options.getGarbageNodesParentCount(); i ++) {
+            garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).setProperty(JcrConstants.JCR_PRIMARYTYPE, "nt:folder", NAME);
+
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                        setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+
+                if (GenerateGarbageHelper.isEmptyProps(options.getGarbageType())) {
+                    garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                            setProperty(EMPTY_PROPERTY_NAME, "bar", STRING);
+                }
+            }
+        }
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+
+
+        //2. Generate garbage nodes - EMPTY_PROPERTIES
+        for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+            for (int j = 0; j < nodesCountUnderParent; j++) {
+                garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                        removeProperty(EMPTY_PROPERTY_NAME);
+            }
+        }
+
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();

Review Comment:
   same here



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "stefan-egli (via GitHub)" <gi...@apache.org>.
stefan-egli commented on PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#issuecomment-2443948541

   > Regarding the explicit check, I don't think this is necessary
   
   Fair enough.
   
   Two unrelated additional comments:
   * the DocumentNodeStore is not properly disposed. You can see this by running the oak-run command consecutively : the second start will have to wait 2min for the lease timeout of the first non-gracefully shut down run. We should ensure a graceful disposal.
   * what might be useful is to add the base path where the garbage is generated to - such that the user immediately knows (and doesn't eg have to consult the source code)


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "rishabhdaim (via GitHub)" <gi...@apache.org>.
rishabhdaim commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1831386331


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/CreateGarbageCommand.java:
##########
@@ -0,0 +1,574 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.base.Joiner;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.commons.concurrent.ExecutorCloser;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.GenerateGarbageHelper;
+import org.apache.jackrabbit.oak.plugins.document.LeaseCheckMode;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.asCloseable;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateGarbageCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.

Review Comment:
   ```suggestion
    * CreateGarbageCommand creates garbage nodes in the repository to allow for testing fullGC functionality.
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "shodaaan (via GitHub)" <gi...@apache.org>.
shodaaan commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1809016352


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateFullGCCommand.java:
##########
@@ -0,0 +1,416 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.plugins.document.Collection;
+import org.apache.jackrabbit.oak.plugins.document.Document;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.DocumentStoreException;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateFullGCCommand implements Command {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateFullGCCommand.class);
+
+    private static final String USAGE = "generateFullGC {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String FULLGC_GEN_ROOT_PATH = "fullGCGenRoot";
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String FULLGC_GEN_BASE_PATH = "fullGCGenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String FULLGC_GEN_PARENT_NODE_PREFIX = "fullGCParent_";
+    public static String FULLGC_GEN_NODE_PREFIX = "fullGCNode_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node FULLGC_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public GenerateFullGCOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public GenerateFullGCOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {
+        Closer closer = Closer.create();
+        try {
+            execute(closer, args);
+        } catch (Throwable e) {
+            LOG.error("Command failed", e);
+            throw closer.rethrow(e);
+        } finally {
+            closer.close();
+        }
+    }
+
+    /**
+     * Method with passed closer is necessary in order to allow for unit tests to check the output of the command.
+     * It is the responsibility of the caller to close the closer.
+     *
+     * Returns the list of generated garbage base paths (under the garbage root node).
+     * @param closer
+     * @param args
+     * @throws Exception
+     */
+    public List<String> execute(Closer closer, String... args) throws Exception {
+        continuousRunIndex = 0;
+
+        List<String> generateBasePaths = new ArrayList<>();
+
+        GenerateFullGCOptions options = new GenerateFullGCOptions(USAGE).parse(args);
+        String subCmd = options.getSubCmd();
+
+        if (GenerateFullGCOptions.CMD_GENERATE.equals(subCmd)) {
+            if (options.getNumberOfRuns() > 1 && options.getGenerateIntervalSeconds() > 0) {
+                generateBasePaths.addAll(generateGarbageContinuously(options, closer));
+            } else {
+                generateBasePaths.add(generateGarbage(options, closer, 0));
+            }
+        } else if (GenerateFullGCOptions.CMD_CLEAN.equals(subCmd)) {
+            cleanGarbage(options, closer);
+        } else {
+            System.err.println("unknown revisions command: " + subCmd);
+        }
+
+        return generateBasePaths;
+    }
+
+    private List<String> generateGarbageContinuously(GenerateFullGCOptions options, Closer closer) throws IOException, Exception {
+        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long startGenTimestamp = System.currentTimeMillis();
+
+        List<String> generatedGarbageBasePaths = new ArrayList<>();
+
+        int numberOfRuns = options.getNumberOfRuns();
+        int intervalSeconds = options.getGenerateIntervalSeconds();
+        Runnable task = () -> {
+            if (continuousRunIndex < numberOfRuns) {
+                try {
+                    String genBasePath = generateGarbage(options, closer, continuousRunIndex, builder, startGenTimestamp);
+                    generatedGarbageBasePaths.add(genBasePath);
+                } catch (Exception e) {
+                    LOG.error("Error generating garbage in run " + continuousRunIndex, e);
+                }
+                LOG.info("Task executed. Count: " + (continuousRunIndex + 1));
+                continuousRunIndex++;
+            } else {
+                // Shutdown the executor once the task has run numberOfRuns times
+                executor.shutdown();
+                LOG.info("Task completed " + numberOfRuns + " times. Stopping execution.");
+            }
+        };
+
+        // Schedule the task to run every intervalSeconds
+        executor.scheduleAtFixedRate(task, 0, intervalSeconds, TimeUnit.SECONDS);
+
+        return generatedGarbageBasePaths;
+    }
+
+    /**
+     * Generate garbage nodes in the repository in order to allow for testing fullGC functionality.
+     *
+     * Returns the path of the generated FULLGC_GEN_BASE_PATH node (under the root).
+     * @param options
+     * @param closer
+     * @param runIndex
+     * @return
+     * @throws IOException
+     * @throws Exception
+     */
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long generationTimestamp = System.currentTimeMillis();
+
+        return generateGarbage(options, closer, runIndex, builder, generationTimestamp);
+    }
+
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex,
+                                  DocumentNodeStoreBuilder<?> builder, long timestamp) throws IOException, Exception {
+
+        if (builder == null) {
+            System.err.println("generateFullGC mode only available for DocumentNodeStore");
+            System.exit(1);
+        }
+
+        String generationBasePath = FULLGC_GEN_BASE_PATH + timestamp + "_" + runIndex;
+        System.out.println("Generating fullGC on the document: " + generationBasePath);
+        documentNodeStore = builder.build();
+
+        VersionGarbageCollector.FullGCMode fullGCMode = getFullGCMode(options);
+        if (fullGCMode == VersionGarbageCollector.FullGCMode.NONE) {
+            LOG.error("Invalid garbageType specified. Must be one of the following: 1 - EMPTYPROPS, 2 - GAP_ORPHANS, 3 - GAP_ORPHANS_EMPTYPROPS");
+            System.exit(1);
+        }
+
+        //1. Create nodes with properties
+        NodeBuilder rootNode = documentNodeStore.getRoot().builder();
+        NodeBuilder garbageRootNode = rootNode.child(FULLGC_GEN_ROOT_PATH);
+        garbageRootNode.child(generationBasePath).setProperty("jcr:primaryType", "nt:file", NAME);
+
+        int nodesCountUnderParent = options.getCreateGarbageNodesCount() / options.getGarbageNodesParentCount();
+        for(int i = 0; i < options.getGarbageNodesParentCount(); i ++) {
+            garbageRootNode.child(generationBasePath).child(FULLGC_GEN_PARENT_NODE_PREFIX + i).setProperty("jcr:primaryType", "nt:folder", NAME);
+
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                garbageRootNode.child(generationBasePath).child(FULLGC_GEN_PARENT_NODE_PREFIX + i).child(FULLGC_GEN_NODE_PREFIX + j).
+                        setProperty("jcr:primaryType", "nt:file", NAME);
+
+                if (fullGCMode == VersionGarbageCollector.FullGCMode.EMPTYPROPS || fullGCMode == VersionGarbageCollector.FullGCMode.GAP_ORPHANS_EMPTYPROPS) {
+                    garbageRootNode.child(generationBasePath).child(FULLGC_GEN_PARENT_NODE_PREFIX + i).child(FULLGC_GEN_NODE_PREFIX + j).
+                            setProperty(EMPTY_PROPERTY_NAME, "bar", STRING);
+                }
+            }
+        }
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);

Review Comment:
   The merge and runBackgroundOperations calls are necessary after each batch of operations in order for the changes to be applied to the MongoDB database.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "rishabhdaim (via GitHub)" <gi...@apache.org>.
rishabhdaim commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1809962935


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/plugins/document/GenerateGarbageHelper.java:
##########
@@ -0,0 +1,19 @@
+package org.apache.jackrabbit.oak.plugins.document;
+
+/**
+ * Helper class to generate garbage for testing purposes.
+ */
+public class GenerateGarbageHelper {
+
+    public static boolean isInvalidGarbageGenerationMode(int fullGCMode) {
+        return fullGCMode == 0;
+    }
+
+    public static boolean includesEmptyProps(int fullGCMode) {

Review Comment:
   ```suggestion
       public static boolean isEmptyProps(int fullGCMode) {
   ```



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateGarbageCommand.java:
##########
@@ -0,0 +1,427 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.commons.concurrent.ExecutorCloser;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.GenerateGarbageHelper;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateGarbageCommand implements Command, Closeable {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateGarbageCommand.class);
+
+    private final ScheduledExecutorService continuousRunExecutor = Executors.newScheduledThreadPool(1);
+
+    private static final String USAGE = "createGarbage {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * FullGC garbage should be generated under tmp node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH_BASE = "tmp";
+
+    /**
+     * The root node name for fullGC garbage generation, one level under tmp.
+     */
+    public static String GARBAGE_GEN_ROOT_NODE_NAME = "fullGC_GarbageRoot";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH = GARBAGE_GEN_ROOT_PATH_BASE + "/" + GARBAGE_GEN_ROOT_NODE_NAME;
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String GEN_BASE_PATH = "GenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String GEN_PARENT_NODE_PREFIX = "Parent_";
+    public static String GEN_NODE_PREFIX = "Node_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node FULLGC_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public GenerateFullGCOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public GenerateFullGCOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {
+
+        try (Closer closer = Closer.create()) {
+            execute(closer, args);
+        } catch (Throwable e) {
+            LOG.error("Command failed", e);
+            throw e;
+        }
+    }
+
+    /**
+     * Method with passed closer is necessary in order to allow for unit tests to check the output of the command.
+     * It is the responsibility of the caller to close the closer.
+     *
+     * Returns the list of generated garbage base paths (under the garbage root node).
+     * @param closer
+     * @param args
+     * @throws Exception
+     */
+    public List<String> execute(Closer closer, String... args) throws Exception {
+        continuousRunIndex = 0;
+
+        List<String> generateBasePaths = new ArrayList<>();
+
+        GenerateFullGCOptions options = new GenerateFullGCOptions(USAGE).parse(args);
+        String subCmd = options.getSubCmd();
+
+        if (GenerateFullGCOptions.CMD_GENERATE.equals(subCmd)) {
+
+            if (options.getCreateGarbageNodesCount() <= 0) {
+                LOG.error("Invalid garbageNodesCount specified: " + options.getCreateGarbageNodesCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+            if (options.getGarbageNodesParentCount() <= 0) {
+                LOG.error("Invalid garbageNodesParentCount specified: " + options.getGarbageNodesParentCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+
+            if (options.getNumberOfRuns() > 1 && options.getGenerateIntervalSeconds() > 0) {
+                generateBasePaths.addAll(generateGarbageContinuously(options, closer));
+            } else {
+                generateBasePaths.add(generateGarbage(options, closer, 0));
+            }
+        } else if (GenerateFullGCOptions.CMD_CLEAN.equals(subCmd)) {
+            cleanGarbage(options, closer);
+        } else {
+            System.err.println("Unknown revisions command: " + subCmd);
+        }
+
+        return generateBasePaths;
+    }
+
+    @Override
+    public void close() throws IOException {
+        new ExecutorCloser(this.continuousRunExecutor).close();
+    }
+
+    private List<String> generateGarbageContinuously(GenerateFullGCOptions options, Closer closer) throws Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long startGenTimestamp = System.currentTimeMillis();
+
+        List<String> generatedGarbageBasePaths = new ArrayList<>();
+
+        int numberOfRuns = options.getNumberOfRuns();
+        int intervalSeconds = options.getGenerateIntervalSeconds();
+        Runnable task = () -> {
+            if (continuousRunIndex < numberOfRuns) {
+                try {
+                    String genBasePath = generateGarbage(options, closer, continuousRunIndex, builder, startGenTimestamp);
+                    generatedGarbageBasePaths.add(genBasePath);
+                } catch (Exception e) {
+                    LOG.error("Error generating garbage in run " + continuousRunIndex, e);
+                }
+                LOG.info("Task executed. Count: " + (continuousRunIndex + 1));
+                continuousRunIndex++;
+            } else {
+                // Shutdown the executor once the task has run numberOfRuns times
+                continuousRunExecutor.shutdown();
+                LOG.info("Task completed " + numberOfRuns + " times. Stopping execution.");
+            }
+        };
+
+        // Schedule the task to run every intervalSeconds
+        continuousRunExecutor.scheduleAtFixedRate(task, 0, intervalSeconds, TimeUnit.SECONDS);
+
+        return generatedGarbageBasePaths;
+    }
+
+    /**
+     * Generate garbage nodes in the repository in order to allow for testing fullGC functionality.
+     *
+     * Returns the path of the generated FULLGC_GEN_BASE_PATH node (under the root).
+     * @param options
+     * @param closer
+     * @param runIndex
+     * @return
+     * @throws IOException
+     * @throws Exception
+     */
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long generationTimestamp = System.currentTimeMillis();
+
+        return generateGarbage(options, closer, runIndex, builder, generationTimestamp);
+    }
+
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex,

Review Comment:
   `closer` is not used in this method.



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/plugins/document/GenerateGarbageHelper.java:
##########
@@ -0,0 +1,19 @@
+package org.apache.jackrabbit.oak.plugins.document;
+
+/**
+ * Helper class to generate garbage for testing purposes.
+ */
+public class GenerateGarbageHelper {
+
+    public static boolean isInvalidGarbageGenerationMode(int fullGCMode) {
+        return fullGCMode == 0;
+    }
+
+    public static boolean includesEmptyProps(int fullGCMode) {
+        return fullGCMode == 1;
+    }
+
+    public static boolean includesGapOrphans(int fullGCMode) {

Review Comment:
   ```suggestion
       public static boolean isGapOrphans(int fullGCMode) {
   ```



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateGarbageCommand.java:
##########
@@ -0,0 +1,427 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.commons.concurrent.ExecutorCloser;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.GenerateGarbageHelper;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateGarbageCommand implements Command, Closeable {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateGarbageCommand.class);
+
+    private final ScheduledExecutorService continuousRunExecutor = Executors.newScheduledThreadPool(1);

Review Comment:
   We need to register this executor with `Closer` to close this.



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateGarbageCommand.java:
##########
@@ -0,0 +1,427 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.commons.concurrent.ExecutorCloser;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.GenerateGarbageHelper;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateGarbageCommand implements Command, Closeable {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateGarbageCommand.class);
+
+    private final ScheduledExecutorService continuousRunExecutor = Executors.newScheduledThreadPool(1);
+
+    private static final String USAGE = "createGarbage {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * FullGC garbage should be generated under tmp node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH_BASE = "tmp";
+
+    /**
+     * The root node name for fullGC garbage generation, one level under tmp.
+     */
+    public static String GARBAGE_GEN_ROOT_NODE_NAME = "fullGC_GarbageRoot";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH = GARBAGE_GEN_ROOT_PATH_BASE + "/" + GARBAGE_GEN_ROOT_NODE_NAME;
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String GEN_BASE_PATH = "GenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String GEN_PARENT_NODE_PREFIX = "Parent_";
+    public static String GEN_NODE_PREFIX = "Node_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node FULLGC_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public GenerateFullGCOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser

Review Comment:
   We should allow it to create multiple types of garbage.
   
   @stefan-egli wdyt?
   
   Here is a sample code to enable delimited values in parameters: https://github.com/apache/jackrabbit-oak/blob/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/run/DataStoreCheckCommand.java#L148-L150 



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateGarbageCommand.java:
##########
@@ -0,0 +1,427 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.commons.concurrent.ExecutorCloser;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.GenerateGarbageHelper;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateGarbageCommand implements Command, Closeable {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateGarbageCommand.class);
+
+    private final ScheduledExecutorService continuousRunExecutor = Executors.newScheduledThreadPool(1);
+
+    private static final String USAGE = "createGarbage {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * FullGC garbage should be generated under tmp node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH_BASE = "tmp";
+
+    /**
+     * The root node name for fullGC garbage generation, one level under tmp.
+     */
+    public static String GARBAGE_GEN_ROOT_NODE_NAME = "fullGC_GarbageRoot";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH = GARBAGE_GEN_ROOT_PATH_BASE + "/" + GARBAGE_GEN_ROOT_NODE_NAME;
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String GEN_BASE_PATH = "GenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String GEN_PARENT_NODE_PREFIX = "Parent_";
+    public static String GEN_NODE_PREFIX = "Node_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node FULLGC_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public GenerateFullGCOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public GenerateFullGCOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {
+
+        try (Closer closer = Closer.create()) {
+            execute(closer, args);
+        } catch (Throwable e) {
+            LOG.error("Command failed", e);
+            throw e;
+        }
+    }
+
+    /**
+     * Method with passed closer is necessary in order to allow for unit tests to check the output of the command.
+     * It is the responsibility of the caller to close the closer.
+     *
+     * Returns the list of generated garbage base paths (under the garbage root node).
+     * @param closer
+     * @param args
+     * @throws Exception
+     */
+    public List<String> execute(Closer closer, String... args) throws Exception {
+        continuousRunIndex = 0;
+
+        List<String> generateBasePaths = new ArrayList<>();
+
+        GenerateFullGCOptions options = new GenerateFullGCOptions(USAGE).parse(args);
+        String subCmd = options.getSubCmd();
+
+        if (GenerateFullGCOptions.CMD_GENERATE.equals(subCmd)) {
+
+            if (options.getCreateGarbageNodesCount() <= 0) {
+                LOG.error("Invalid garbageNodesCount specified: " + options.getCreateGarbageNodesCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+            if (options.getGarbageNodesParentCount() <= 0) {
+                LOG.error("Invalid garbageNodesParentCount specified: " + options.getGarbageNodesParentCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+
+            if (options.getNumberOfRuns() > 1 && options.getGenerateIntervalSeconds() > 0) {
+                generateBasePaths.addAll(generateGarbageContinuously(options, closer));
+            } else {
+                generateBasePaths.add(generateGarbage(options, closer, 0));
+            }
+        } else if (GenerateFullGCOptions.CMD_CLEAN.equals(subCmd)) {
+            cleanGarbage(options, closer);
+        } else {
+            System.err.println("Unknown revisions command: " + subCmd);
+        }
+
+        return generateBasePaths;
+    }
+
+    @Override
+    public void close() throws IOException {
+        new ExecutorCloser(this.continuousRunExecutor).close();
+    }
+
+    private List<String> generateGarbageContinuously(GenerateFullGCOptions options, Closer closer) throws Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long startGenTimestamp = System.currentTimeMillis();
+
+        List<String> generatedGarbageBasePaths = new ArrayList<>();
+
+        int numberOfRuns = options.getNumberOfRuns();
+        int intervalSeconds = options.getGenerateIntervalSeconds();
+        Runnable task = () -> {
+            if (continuousRunIndex < numberOfRuns) {
+                try {
+                    String genBasePath = generateGarbage(options, closer, continuousRunIndex, builder, startGenTimestamp);
+                    generatedGarbageBasePaths.add(genBasePath);
+                } catch (Exception e) {
+                    LOG.error("Error generating garbage in run " + continuousRunIndex, e);
+                }
+                LOG.info("Task executed. Count: " + (continuousRunIndex + 1));
+                continuousRunIndex++;
+            } else {
+                // Shutdown the executor once the task has run numberOfRuns times
+                continuousRunExecutor.shutdown();
+                LOG.info("Task completed " + numberOfRuns + " times. Stopping execution.");
+            }
+        };
+
+        // Schedule the task to run every intervalSeconds
+        continuousRunExecutor.scheduleAtFixedRate(task, 0, intervalSeconds, TimeUnit.SECONDS);
+
+        return generatedGarbageBasePaths;
+    }
+
+    /**
+     * Generate garbage nodes in the repository in order to allow for testing fullGC functionality.
+     *
+     * Returns the path of the generated FULLGC_GEN_BASE_PATH node (under the root).
+     * @param options
+     * @param closer
+     * @param runIndex
+     * @return
+     * @throws IOException
+     * @throws Exception
+     */
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long generationTimestamp = System.currentTimeMillis();
+
+        return generateGarbage(options, closer, runIndex, builder, generationTimestamp);
+    }
+
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex,
+                                  DocumentNodeStoreBuilder<?> builder, long timestamp) throws IOException, Exception {
+
+        if (builder == null) {
+            System.err.println("generateFullGC mode only available for DocumentNodeStore");
+            System.exit(1);
+        }
+
+        String generationBasePath = GEN_BASE_PATH + timestamp + "_" + runIndex;
+        System.out.println("Generating fullGC on the document: " + generationBasePath);
+        documentNodeStore = builder.build();
+
+        //VersionGarbageCollector.FullGCMode fullGCMode = getFullGCMode(options);
+        if (GenerateGarbageHelper.isInvalidGarbageGenerationMode(options.getGarbageType())) {
+            LOG.error("Invalid fullGCMode specified: " + options.getGarbageType() + " in: " + getClass().getName());
+            System.exit(1);
+        }
+
+        //1. Create nodes with properties
+        NodeBuilder rootNode = documentNodeStore.getRoot().builder();
+        NodeBuilder garbageRootNode = rootNode.child(GARBAGE_GEN_ROOT_PATH_BASE).child(GARBAGE_GEN_ROOT_NODE_NAME);
+        garbageRootNode.child(generationBasePath).setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+
+        int nodesCountUnderParent = options.getCreateGarbageNodesCount() / options.getGarbageNodesParentCount();
+        for(int i = 0; i < options.getGarbageNodesParentCount(); i ++) {
+            garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).setProperty(JcrConstants.JCR_PRIMARYTYPE, "nt:folder", NAME);
+
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                        setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+
+                if (GenerateGarbageHelper.includesEmptyProps(options.getGarbageType())) {
+                    garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                            setProperty(EMPTY_PROPERTY_NAME, "bar", STRING);
+                }
+            }
+        }
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+
+
+        //2. Generate garbage nodes - EMPTY_PROPERTIES
+        if (GenerateGarbageHelper.includesEmptyProps(options.getGarbageType())) {
+            for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+                for (int j = 0; j < nodesCountUnderParent; j++) {
+                    garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                            removeProperty(EMPTY_PROPERTY_NAME);
+                }
+            }
+        }
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+
+        //3.1. Generate garbage nodes - GAP_ORPHANS - remove parent nodes
+        if (GenerateGarbageHelper.includesGapOrphans(options.getGarbageType())) {

Review Comment:
   A sample implementation could be like this.
   
   `private interface GarbageType {
           void createGarbage();
       }`
   
       `private class EmptyProps implements GarbageType {
   
           @Override
           public void createGarbage() {
               for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
                   for (int j = 0; j < nodesCountUnderParent; j++) {
                       garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
                               removeProperty(EMPTY_PROPERTY_NAME);
                   }
               }
   
               documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
               documentNodeStore.runBackgroundOperations();
           }
       }`



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateGarbageCommand.java:
##########
@@ -0,0 +1,427 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.commons.concurrent.ExecutorCloser;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.GenerateGarbageHelper;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateGarbageCommand implements Command, Closeable {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateGarbageCommand.class);
+
+    private final ScheduledExecutorService continuousRunExecutor = Executors.newScheduledThreadPool(1);
+
+    private static final String USAGE = "createGarbage {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * FullGC garbage should be generated under tmp node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH_BASE = "tmp";
+
+    /**
+     * The root node name for fullGC garbage generation, one level under tmp.
+     */
+    public static String GARBAGE_GEN_ROOT_NODE_NAME = "fullGC_GarbageRoot";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH = GARBAGE_GEN_ROOT_PATH_BASE + "/" + GARBAGE_GEN_ROOT_NODE_NAME;
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String GEN_BASE_PATH = "GenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String GEN_PARENT_NODE_PREFIX = "Parent_";
+    public static String GEN_NODE_PREFIX = "Node_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node FULLGC_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public GenerateFullGCOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public GenerateFullGCOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {
+
+        try (Closer closer = Closer.create()) {
+            execute(closer, args);
+        } catch (Throwable e) {
+            LOG.error("Command failed", e);
+            throw e;
+        }
+    }
+
+    /**
+     * Method with passed closer is necessary in order to allow for unit tests to check the output of the command.
+     * It is the responsibility of the caller to close the closer.
+     *
+     * Returns the list of generated garbage base paths (under the garbage root node).
+     * @param closer
+     * @param args
+     * @throws Exception
+     */
+    public List<String> execute(Closer closer, String... args) throws Exception {
+        continuousRunIndex = 0;
+
+        List<String> generateBasePaths = new ArrayList<>();
+
+        GenerateFullGCOptions options = new GenerateFullGCOptions(USAGE).parse(args);
+        String subCmd = options.getSubCmd();
+
+        if (GenerateFullGCOptions.CMD_GENERATE.equals(subCmd)) {
+
+            if (options.getCreateGarbageNodesCount() <= 0) {
+                LOG.error("Invalid garbageNodesCount specified: " + options.getCreateGarbageNodesCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+            if (options.getGarbageNodesParentCount() <= 0) {
+                LOG.error("Invalid garbageNodesParentCount specified: " + options.getGarbageNodesParentCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+
+            if (options.getNumberOfRuns() > 1 && options.getGenerateIntervalSeconds() > 0) {
+                generateBasePaths.addAll(generateGarbageContinuously(options, closer));
+            } else {
+                generateBasePaths.add(generateGarbage(options, closer, 0));
+            }
+        } else if (GenerateFullGCOptions.CMD_CLEAN.equals(subCmd)) {
+            cleanGarbage(options, closer);
+        } else {
+            System.err.println("Unknown revisions command: " + subCmd);
+        }
+
+        return generateBasePaths;
+    }
+
+    @Override
+    public void close() throws IOException {
+        new ExecutorCloser(this.continuousRunExecutor).close();
+    }
+
+    private List<String> generateGarbageContinuously(GenerateFullGCOptions options, Closer closer) throws Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long startGenTimestamp = System.currentTimeMillis();
+
+        List<String> generatedGarbageBasePaths = new ArrayList<>();
+
+        int numberOfRuns = options.getNumberOfRuns();
+        int intervalSeconds = options.getGenerateIntervalSeconds();
+        Runnable task = () -> {
+            if (continuousRunIndex < numberOfRuns) {
+                try {
+                    String genBasePath = generateGarbage(options, closer, continuousRunIndex, builder, startGenTimestamp);
+                    generatedGarbageBasePaths.add(genBasePath);
+                } catch (Exception e) {
+                    LOG.error("Error generating garbage in run " + continuousRunIndex, e);
+                }
+                LOG.info("Task executed. Count: " + (continuousRunIndex + 1));
+                continuousRunIndex++;
+            } else {
+                // Shutdown the executor once the task has run numberOfRuns times
+                continuousRunExecutor.shutdown();
+                LOG.info("Task completed " + numberOfRuns + " times. Stopping execution.");
+            }
+        };
+
+        // Schedule the task to run every intervalSeconds
+        continuousRunExecutor.scheduleAtFixedRate(task, 0, intervalSeconds, TimeUnit.SECONDS);
+
+        return generatedGarbageBasePaths;
+    }
+
+    /**
+     * Generate garbage nodes in the repository in order to allow for testing fullGC functionality.
+     *
+     * Returns the path of the generated FULLGC_GEN_BASE_PATH node (under the root).
+     * @param options
+     * @param closer
+     * @param runIndex
+     * @return
+     * @throws IOException
+     * @throws Exception
+     */
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long generationTimestamp = System.currentTimeMillis();
+
+        return generateGarbage(options, closer, runIndex, builder, generationTimestamp);
+    }
+
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex,
+                                  DocumentNodeStoreBuilder<?> builder, long timestamp) throws IOException, Exception {
+
+        if (builder == null) {
+            System.err.println("generateFullGC mode only available for DocumentNodeStore");
+            System.exit(1);
+        }
+
+        String generationBasePath = GEN_BASE_PATH + timestamp + "_" + runIndex;
+        System.out.println("Generating fullGC on the document: " + generationBasePath);
+        documentNodeStore = builder.build();
+
+        //VersionGarbageCollector.FullGCMode fullGCMode = getFullGCMode(options);
+        if (GenerateGarbageHelper.isInvalidGarbageGenerationMode(options.getGarbageType())) {
+            LOG.error("Invalid fullGCMode specified: " + options.getGarbageType() + " in: " + getClass().getName());
+            System.exit(1);
+        }
+
+        //1. Create nodes with properties
+        NodeBuilder rootNode = documentNodeStore.getRoot().builder();
+        NodeBuilder garbageRootNode = rootNode.child(GARBAGE_GEN_ROOT_PATH_BASE).child(GARBAGE_GEN_ROOT_NODE_NAME);
+        garbageRootNode.child(generationBasePath).setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+
+        int nodesCountUnderParent = options.getCreateGarbageNodesCount() / options.getGarbageNodesParentCount();
+        for(int i = 0; i < options.getGarbageNodesParentCount(); i ++) {
+            garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).setProperty(JcrConstants.JCR_PRIMARYTYPE, "nt:folder", NAME);
+
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                        setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+
+                if (GenerateGarbageHelper.includesEmptyProps(options.getGarbageType())) {
+                    garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                            setProperty(EMPTY_PROPERTY_NAME, "bar", STRING);
+                }
+            }
+        }
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+
+
+        //2. Generate garbage nodes - EMPTY_PROPERTIES
+        if (GenerateGarbageHelper.includesEmptyProps(options.getGarbageType())) {
+            for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+                for (int j = 0; j < nodesCountUnderParent; j++) {
+                    garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                            removeProperty(EMPTY_PROPERTY_NAME);
+                }
+            }
+        }
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+
+        //3.1. Generate garbage nodes - GAP_ORPHANS - remove parent nodes
+        if (GenerateGarbageHelper.includesGapOrphans(options.getGarbageType())) {

Review Comment:
   One problem with this approach is that the more garbage types we add in the future, the more if/else statements we need to add.
   
   This would make the code more complex, less readable, and very hard to understand.
   
   How about we create `enums` for fullGcMode with `createGarbage` method and each mode would create its type of garbage?
   
   This would ensure that for adding a new type of garbage, we don't need to update the base class, add a new implementation of garbage type, and plug in the base class.
   
   cc @stefan-egli @Joscorbe 
   
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "stefan-egli (via GitHub)" <gi...@apache.org>.
stefan-egli commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1819282251


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateGarbageCommand.java:
##########
@@ -0,0 +1,553 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.commons.concurrent.ExecutorCloser;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.GenerateGarbageHelper;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateGarbageCommand implements Command, Closeable {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateGarbageCommand.class);
+
+    private final ScheduledExecutorService continuousRunExecutor = Executors.newScheduledThreadPool(1);
+
+    private static final String USAGE = "createGarbage {<jdbc-uri> | <mongodb-uri>} [options]";

Review Comment:
   should use the actual command instead of `createGarbage`.
   
   Also, somewhere, not sure where, mandatory options should be mentioned, such as IIUC `garbageNodesCount` at least.



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateGarbageCommand.java:
##########
@@ -0,0 +1,553 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.commons.concurrent.ExecutorCloser;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.GenerateGarbageHelper;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateGarbageCommand implements Command, Closeable {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateGarbageCommand.class);
+
+    private final ScheduledExecutorService continuousRunExecutor = Executors.newScheduledThreadPool(1);
+
+    private static final String USAGE = "createGarbage {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * FullGC garbage should be generated under tmp node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH_BASE = "tmp";
+
+    /**
+     * The root node name for fullGC garbage generation, one level under tmp.
+     */
+    public static String GARBAGE_GEN_ROOT_NODE_NAME = "fullGC_GarbageRoot";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH = GARBAGE_GEN_ROOT_PATH_BASE + "/" + GARBAGE_GEN_ROOT_NODE_NAME;
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String GEN_BASE_PATH = "GenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String GEN_PARENT_NODE_PREFIX = "Parent_";
+    public static String GEN_NODE_PREFIX = "Node_";
+    public static String GEN_NODE_LEVEL_PREFIX = "_Level_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    /**
+     * The maximum depth in the tree at which to generate gap orphans garbage nodes.
+     * Used for validation of the orphansDepth / orphansLevelGap options.
+     */
+    private final int GAP_ORPHANS_MAX_DEPTH = 15;
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node FULLGC_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> orphansDepth;
+        final OptionSpec<Integer> orphansLevelGap;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public GenerateFullGCOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            orphansDepth = parser
+                    .accepts("orphansDepth", "the depth in the tree at which to create garbage nodes. Only applies to ORPHANS fullGC modes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            orphansLevelGap = parser
+                    .accepts("orphansLevelGap", "the gap in the tree between the first leaf gap orphan garbage node and its' parent. Only applies to ORPHANS fullGC modes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public GenerateFullGCOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getOrphansDepth() { return orphansDepth.value(options);}
+
+        public int getOrphansLevelGap() { return orphansLevelGap.value(options); }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {
+
+        try (Closer closer = Closer.create()) {
+            // Register the ExecutorService with Closer
+            closer.register(() -> {
+                // Properly shutdown the executor service
+                continuousRunExecutor.shutdown();
+                try {
+                    if (!continuousRunExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
+                        continuousRunExecutor.shutdownNow();
+                    }
+                } catch (InterruptedException e) {
+                    throw new RuntimeException(e);
+                }
+            });
+            execute(closer, args);
+        } catch (Throwable e) {
+            LOG.error("Command failed", e);
+            throw e;
+        }
+    }
+
+    /**
+     * Method with passed closer is necessary in order to allow for unit tests to check the output of the command.
+     * It is the responsibility of the caller to close the closer.
+     *
+     * Returns the list of generated garbage base paths (under the garbage root node).
+     * @param closer
+     * @param args
+     * @throws Exception
+     */
+    public List<String> execute(Closer closer, String... args) throws Exception {
+        continuousRunIndex = 0;
+
+        List<String> generateBasePaths = new ArrayList<>();
+
+        GenerateFullGCOptions options = new GenerateFullGCOptions(USAGE).parse(args);
+        String subCmd = options.getSubCmd();
+
+        if (GenerateFullGCOptions.CMD_GENERATE.equals(subCmd)) {
+
+            if (options.getCreateGarbageNodesCount() <= 0) {
+                LOG.error("Invalid garbageNodesCount specified: " + options.getCreateGarbageNodesCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+            if (options.getGarbageNodesParentCount() <= 0) {
+                LOG.error("Invalid garbageNodesParentCount specified: " + options.getGarbageNodesParentCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+
+            if (options.getNumberOfRuns() > 1 && options.getGenerateIntervalSeconds() > 0) {
+                generateBasePaths.addAll(generateGarbageContinuously(options, closer));
+            } else {
+                generateBasePaths.add(generateGarbage(options, closer, 0));
+            }
+        } else if (GenerateFullGCOptions.CMD_CLEAN.equals(subCmd)) {
+            cleanGarbage(options, closer);
+        } else {
+            System.err.println("Unknown revisions command: " + subCmd);
+        }
+
+        return generateBasePaths;
+    }
+
+    @Override
+    public void close() throws IOException {
+        new ExecutorCloser(this.continuousRunExecutor).close();
+    }
+
+    private List<String> generateGarbageContinuously(GenerateFullGCOptions options, Closer closer) throws Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long startGenTimestamp = System.currentTimeMillis();
+
+        List<String> generatedGarbageBasePaths = new ArrayList<>();
+
+        int numberOfRuns = options.getNumberOfRuns();
+        int intervalSeconds = options.getGenerateIntervalSeconds();
+        Runnable task = () -> {
+            if (continuousRunIndex < numberOfRuns) {
+                try {
+                    String genBasePath = generateGarbage(options, continuousRunIndex, builder, startGenTimestamp);
+                    generatedGarbageBasePaths.add(genBasePath);
+                } catch (Exception e) {
+                    LOG.error("Error generating garbage in run " + continuousRunIndex, e);
+                }
+                LOG.info("Task executed. Count: " + (continuousRunIndex + 1));
+                continuousRunIndex++;
+            } else {
+                // Shutdown the executor once the task has run numberOfRuns times
+                continuousRunExecutor.shutdown();
+                LOG.info("Task completed " + numberOfRuns + " times. Stopping execution.");
+            }
+        };
+
+        // Schedule the task to run every intervalSeconds
+        continuousRunExecutor.scheduleAtFixedRate(task, 0, intervalSeconds, TimeUnit.SECONDS);
+
+        return generatedGarbageBasePaths;
+    }
+
+    /**
+     * Generate garbage nodes in the repository in order to allow for testing fullGC functionality.
+     *
+     * Returns the path of the generated FULLGC_GEN_BASE_PATH node (under the root).
+     * @param options
+     * @param closer
+     * @param runIndex
+     * @return
+     * @throws IOException
+     * @throws Exception
+     */
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long generationTimestamp = System.currentTimeMillis();
+
+        return generateGarbage(options, runIndex, builder, generationTimestamp);
+    }
+
+    private String generateGarbage(GenerateFullGCOptions options, int runIndex,
+                                   DocumentNodeStoreBuilder<?> builder, long timestamp) throws Exception {
+
+        if (builder == null) {
+            System.err.println("generateFullGC mode only available for DocumentNodeStore");
+            System.exit(1);
+        }
+
+        if (GenerateGarbageHelper.isInvalidGarbageGenerationMode(options.getGarbageType())) {
+            LOG.error("Invalid fullGCMode specified: " + options.getGarbageType() + " in: " + getClass().getName());
+            System.exit(1);
+        }
+
+        String generationBasePath = GEN_BASE_PATH + timestamp + "_" + runIndex;
+        documentNodeStore = builder.build();
+
+        NodeBuilder rootNode = documentNodeStore.getRoot().builder();
+
+        if (GenerateGarbageHelper.isEmptyProps(options.getGarbageType())) {
+            generateGarbageEmptyProps(rootNode, options, generationBasePath);
+        }
+        else if (GenerateGarbageHelper.isGapOrphans(options.getGarbageType())) {
+            generateGarbageGapOrphans(rootNode, options, generationBasePath);
+        }
+
+        return generationBasePath;
+    }
+    private void generateGarbageGapOrphans(NodeBuilder rootNode, GenerateFullGCOptions options, String generationBasePath)
+            throws CommitFailedException {
+
+        // validate gap orphans depth and level gap
+        if (options.getOrphansDepth() < 0 || options.getOrphansDepth() > GAP_ORPHANS_MAX_DEPTH) {
+            LOG.error("Invalid orphansDepth specified: " + options.getOrphansDepth() + " in: " + getClass().getName()
+                    + ". Must be an integer >= 0 and <= " + GAP_ORPHANS_MAX_DEPTH);
+            System.exit(1);
+        }
+        if (options.getOrphansLevelGap() < 0 || options.getOrphansLevelGap() > options.getOrphansDepth()) {
+            LOG.error("Invalid orphansLevelGap specified: " + options.getOrphansLevelGap() + " in: " + getClass().getName()
+                    + ". Must be a positive integer <= orphansDepth");
+            System.exit(1);
+        }
+
+        System.out.println("Generating fullGC gap orphans on the document: " + generationBasePath);
+
+        NodeBuilder garbageRootNode = rootNode.child(GARBAGE_GEN_ROOT_PATH_BASE).child(GARBAGE_GEN_ROOT_NODE_NAME);
+        garbageRootNode.child(generationBasePath).setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+
+        int nodesCountUnderParent = options.getCreateGarbageNodesCount() / options.getGarbageNodesParentCount();
+        for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+            // create parent node
+            garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).setProperty(JcrConstants.JCR_PRIMARYTYPE, "nt:folder", NAME);
+
+            // create child nodes under parent, according to gap orphans depth
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                getGapOrphanLeafGarbageNode(garbageRootNode, generationBasePath, options.getOrphansDepth(), i, j).
+                        setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+            }
+        }
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+
+        // Generate garbage nodes - GAP_ORPHANS - remove parent nodes
+        StringBuilder sbNodePath = new StringBuilder();
+        List<String> deleteNodePaths = new ArrayList<>();
+        for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+
+            // append the parent to the paths to delete
+            sbNodePath.setLength(0);
+            String path = getIdFromPath(
+                    sbNodePath.append("/").append(GARBAGE_GEN_ROOT_PATH).append("/").append(generationBasePath).append("/")
+                            .append(GEN_PARENT_NODE_PREFIX).append(i).toString());
+            deleteNodePaths.add(path);
+
+            // append all the gap orphans nodes between the parent and the level gap to the paths to delete
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                deleteNodePaths.addAll(generateGapOrphanNodePaths(generationBasePath, i, j, options.getOrphansDepth(), options.getOrphansLevelGap(), false));
+            }
+        }
+        // Remove all parent nodes
+        documentNodeStore.getDocumentStore().remove(org.apache.jackrabbit.oak.plugins.document.Collection.NODES, deleteNodePaths);
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+    }
+
+    private NodeBuilder getGapOrphanLeafGarbageNode(NodeBuilder garbageRootNode, String generationBasePath, int depth, int parentIndex, int nodeIndex) {
+        NodeBuilder garbageNode = garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + parentIndex);
+        for (int i = 1; i < depth; i++) {
+            garbageNode = garbageNode.child(GEN_NODE_PREFIX + nodeIndex + GEN_NODE_LEVEL_PREFIX + i);
+        }
+        // the leaf garbage node does not have level in its name
+        garbageNode = garbageNode.child(GEN_NODE_PREFIX + nodeIndex);
+        return garbageNode;
+    }
+
+    public static List<String> generateGapOrphanNodePaths(String generationBasePath, int parentIndex, int nodeIndex, int gapDepth) {
+        return generateGapOrphanNodePaths(generationBasePath, parentIndex, nodeIndex, gapDepth, 0, false);
+    }
+
+    /**
+     * Gets the paths of the nodes between the parent node and the level gap OR between the level gap and the leaf
+     * gap orphan node.
+     *
+     * Example 1: gapDepth = 4, gapLevelGap = 2, fromLevelGapToLeaf = false will return:
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2
+     * and will NOT return the rest of the nodes:
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3/Node_0 (the leaf node)
+     *
+     * Example 2: gapDepth = 5, gapLevelGap = 2, fromLevelGapToLeaf = true will return:
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3/Node_0_Level_4
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3/Node_0_Level_4/Node_0 (the leaf node)
+     * and will NOT return the nodes from the parent to the gap level:
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2
+     *
+     * @param generationBasePath
+     * @param parentIndex
+     * @param nodeIndex
+     * @param gapDepth - the depth in the tree until which to create gap orphan garbage nodes.
+     * @param gapLevelGap - the gap in the tree between the first gap orphan garbage node and its parent.
+     * @param fromLevelGapToLeaf - if true, the paths will be generated from the level gap to the leaf node, otherwise the paths will be generated from the parent node to the level gap.
+     * @return
+     */
+    public static List<String> generateGapOrphanNodePaths(String generationBasePath, int parentIndex, int nodeIndex, int gapDepth, int gapLevelGap, boolean fromLevelGapToLeaf) {
+        List<String> gapOrphanNodePaths = new ArrayList<>();
+
+        StringBuilder sbNodePath = new StringBuilder();
+        sbNodePath.setLength(0);
+
+        // start path from the parent node
+        sbNodePath.append("/").append(GARBAGE_GEN_ROOT_PATH).append("/").append(generationBasePath).append("/")
+                .append(GEN_PARENT_NODE_PREFIX).append(parentIndex);
+
+        for(int i = 1; i <= gapDepth; i ++) {
+            sbNodePath.append("/").append(GEN_NODE_PREFIX).append(nodeIndex);
+            // the leaf node does NOT have level in its name
+            if (i < gapDepth) {
+                sbNodePath.append(GEN_NODE_LEVEL_PREFIX).append(i);
+            }
+
+            // only append to list if the criteria for returning the node path is met
+            if ((!fromLevelGapToLeaf && i < gapLevelGap) || (fromLevelGapToLeaf && i >= gapLevelGap)) {
+                gapOrphanNodePaths.add(getIdFromPath(sbNodePath.toString()));
+            }
+        }
+
+        return gapOrphanNodePaths;
+    }
+
+    private void generateGarbageEmptyProps(NodeBuilder rootNode, GenerateFullGCOptions options, String generationBasePath) throws CommitFailedException {
+
+        System.out.println("Generating fullGC empty props on the document: " + generationBasePath);
+
+        NodeBuilder garbageRootNode = rootNode.child(GARBAGE_GEN_ROOT_PATH_BASE).child(GARBAGE_GEN_ROOT_NODE_NAME);
+        garbageRootNode.child(generationBasePath).setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+
+        int nodesCountUnderParent = options.getCreateGarbageNodesCount() / options.getGarbageNodesParentCount();
+        for(int i = 0; i < options.getGarbageNodesParentCount(); i ++) {
+            garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).setProperty(JcrConstants.JCR_PRIMARYTYPE, "nt:folder", NAME);
+
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                        setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+
+                if (GenerateGarbageHelper.isEmptyProps(options.getGarbageType())) {
+                    garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                            setProperty(EMPTY_PROPERTY_NAME, "bar", STRING);
+                }
+            }
+        }
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+
+
+        //2. Generate garbage nodes - EMPTY_PROPERTIES
+        for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+            for (int j = 0; j < nodesCountUnderParent; j++) {
+                garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                        removeProperty(EMPTY_PROPERTY_NAME);
+            }
+        }
+
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+    }
+
+    /**
+     * Cleans up all generated garbage by removing the node FULLGC_GEN_ROOT_PATH and all of
+     * its children (recursively)
+     * @param options
+     * @param closer
+     * @throws IOException
+     * @throws Exception
+     */
+    private void cleanGarbage(GenerateFullGCOptions options, Closer closer) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+
+        if (builder == null) {
+            System.err.println("generateFullGC mode only available for DocumentNodeStore");
+            System.exit(1);
+        }
+
+        System.out.println("Cleaning up all generated garbage:");
+        documentNodeStore = builder.build();
+
+        NodeBuilder rootBuilder = documentNodeStore.getRoot().builder();
+
+        NodeBuilder generatedGarbageRootBuilder = rootBuilder.child(GARBAGE_GEN_ROOT_PATH_BASE).child(GARBAGE_GEN_ROOT_NODE_NAME);
+
+        String garbageRootNodePath = "2:/"+ GARBAGE_GEN_ROOT_PATH;
+        List<String> childNodePaths = new ArrayList<>();
+        childNodePaths.add(garbageRootNodePath);
+
+        // get all paths of the tree nodes under the garbage root node
+        getTreeNodePaths(generatedGarbageRootBuilder, garbageRootNodePath, childNodePaths, 2);
+
+        documentNodeStore.getDocumentStore().remove(org.apache.jackrabbit.oak.plugins.document.Collection.NODES, childNodePaths);
+        documentNodeStore.merge(rootBuilder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();

Review Comment:
   (same as above, shouldn't be necessary)



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/AvailableModes.java:
##########
@@ -75,5 +75,6 @@ public final class AvailableModes {
             .put("segment-copy", new SegmentCopyCommand())
             .put("server", new ServerCommand())
             .put("purge-index-versions", new LucenePurgeOldIndexVersionCommand())
+            .put("generateGarbage", new GenerateGarbageCommand())

Review Comment:
   oak-run commands need to be lower case. for readability it might be easier to use something like `"generate-garbage"`?
   
   Or .. on second thought .. what about expressing more explicitly that this new command is meant for testing only by .. eg calling it `generate-test-garbage` ?



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateGarbageCommand.java:
##########
@@ -0,0 +1,553 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.commons.concurrent.ExecutorCloser;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.GenerateGarbageHelper;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateGarbageCommand implements Command, Closeable {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateGarbageCommand.class);
+
+    private final ScheduledExecutorService continuousRunExecutor = Executors.newScheduledThreadPool(1);
+
+    private static final String USAGE = "createGarbage {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * FullGC garbage should be generated under tmp node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH_BASE = "tmp";
+
+    /**
+     * The root node name for fullGC garbage generation, one level under tmp.
+     */
+    public static String GARBAGE_GEN_ROOT_NODE_NAME = "fullGC_GarbageRoot";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH = GARBAGE_GEN_ROOT_PATH_BASE + "/" + GARBAGE_GEN_ROOT_NODE_NAME;
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String GEN_BASE_PATH = "GenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String GEN_PARENT_NODE_PREFIX = "Parent_";
+    public static String GEN_NODE_PREFIX = "Node_";
+    public static String GEN_NODE_LEVEL_PREFIX = "_Level_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    /**
+     * The maximum depth in the tree at which to generate gap orphans garbage nodes.
+     * Used for validation of the orphansDepth / orphansLevelGap options.
+     */
+    private final int GAP_ORPHANS_MAX_DEPTH = 15;
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {

Review Comment:
   I think this shouldn't have "FullGC" in the name anymore?



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/plugins/document/GenerateGarbageHelper.java:
##########
@@ -0,0 +1,19 @@
+package org.apache.jackrabbit.oak.plugins.document;

Review Comment:
   license header is missing



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateGarbageCommand.java:
##########
@@ -0,0 +1,553 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.commons.concurrent.ExecutorCloser;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.GenerateGarbageHelper;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateGarbageCommand implements Command, Closeable {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateGarbageCommand.class);
+
+    private final ScheduledExecutorService continuousRunExecutor = Executors.newScheduledThreadPool(1);
+
+    private static final String USAGE = "createGarbage {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * FullGC garbage should be generated under tmp node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH_BASE = "tmp";
+
+    /**
+     * The root node name for fullGC garbage generation, one level under tmp.
+     */
+    public static String GARBAGE_GEN_ROOT_NODE_NAME = "fullGC_GarbageRoot";

Review Comment:
   not sure about the name here, and whether it should have `fullGC` in it...
   what about `oak-run-generated-test-garbage` ?



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateGarbageCommand.java:
##########
@@ -0,0 +1,553 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.commons.concurrent.ExecutorCloser;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.GenerateGarbageHelper;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateGarbageCommand implements Command, Closeable {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateGarbageCommand.class);
+
+    private final ScheduledExecutorService continuousRunExecutor = Executors.newScheduledThreadPool(1);
+
+    private static final String USAGE = "createGarbage {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * FullGC garbage should be generated under tmp node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH_BASE = "tmp";
+
+    /**
+     * The root node name for fullGC garbage generation, one level under tmp.
+     */
+    public static String GARBAGE_GEN_ROOT_NODE_NAME = "fullGC_GarbageRoot";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH = GARBAGE_GEN_ROOT_PATH_BASE + "/" + GARBAGE_GEN_ROOT_NODE_NAME;
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String GEN_BASE_PATH = "GenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String GEN_PARENT_NODE_PREFIX = "Parent_";
+    public static String GEN_NODE_PREFIX = "Node_";
+    public static String GEN_NODE_LEVEL_PREFIX = "_Level_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    /**
+     * The maximum depth in the tree at which to generate gap orphans garbage nodes.
+     * Used for validation of the orphansDepth / orphansLevelGap options.
+     */
+    private final int GAP_ORPHANS_MAX_DEPTH = 15;
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node FULLGC_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> orphansDepth;
+        final OptionSpec<Integer> orphansLevelGap;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public GenerateFullGCOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            orphansDepth = parser
+                    .accepts("orphansDepth", "the depth in the tree at which to create garbage nodes. Only applies to ORPHANS fullGC modes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            orphansLevelGap = parser
+                    .accepts("orphansLevelGap", "the gap in the tree between the first leaf gap orphan garbage node and its' parent. Only applies to ORPHANS fullGC modes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public GenerateFullGCOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getOrphansDepth() { return orphansDepth.value(options);}
+
+        public int getOrphansLevelGap() { return orphansLevelGap.value(options); }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {
+
+        try (Closer closer = Closer.create()) {
+            // Register the ExecutorService with Closer
+            closer.register(() -> {
+                // Properly shutdown the executor service
+                continuousRunExecutor.shutdown();
+                try {
+                    if (!continuousRunExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
+                        continuousRunExecutor.shutdownNow();
+                    }
+                } catch (InterruptedException e) {
+                    throw new RuntimeException(e);
+                }
+            });
+            execute(closer, args);
+        } catch (Throwable e) {
+            LOG.error("Command failed", e);
+            throw e;
+        }
+    }
+
+    /**
+     * Method with passed closer is necessary in order to allow for unit tests to check the output of the command.
+     * It is the responsibility of the caller to close the closer.
+     *
+     * Returns the list of generated garbage base paths (under the garbage root node).
+     * @param closer
+     * @param args
+     * @throws Exception
+     */
+    public List<String> execute(Closer closer, String... args) throws Exception {
+        continuousRunIndex = 0;
+
+        List<String> generateBasePaths = new ArrayList<>();
+
+        GenerateFullGCOptions options = new GenerateFullGCOptions(USAGE).parse(args);
+        String subCmd = options.getSubCmd();
+
+        if (GenerateFullGCOptions.CMD_GENERATE.equals(subCmd)) {
+
+            if (options.getCreateGarbageNodesCount() <= 0) {
+                LOG.error("Invalid garbageNodesCount specified: " + options.getCreateGarbageNodesCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+            if (options.getGarbageNodesParentCount() <= 0) {
+                LOG.error("Invalid garbageNodesParentCount specified: " + options.getGarbageNodesParentCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+
+            if (options.getNumberOfRuns() > 1 && options.getGenerateIntervalSeconds() > 0) {
+                generateBasePaths.addAll(generateGarbageContinuously(options, closer));
+            } else {
+                generateBasePaths.add(generateGarbage(options, closer, 0));
+            }
+        } else if (GenerateFullGCOptions.CMD_CLEAN.equals(subCmd)) {
+            cleanGarbage(options, closer);
+        } else {
+            System.err.println("Unknown revisions command: " + subCmd);
+        }
+
+        return generateBasePaths;
+    }
+
+    @Override
+    public void close() throws IOException {
+        new ExecutorCloser(this.continuousRunExecutor).close();
+    }
+
+    private List<String> generateGarbageContinuously(GenerateFullGCOptions options, Closer closer) throws Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long startGenTimestamp = System.currentTimeMillis();
+
+        List<String> generatedGarbageBasePaths = new ArrayList<>();
+
+        int numberOfRuns = options.getNumberOfRuns();
+        int intervalSeconds = options.getGenerateIntervalSeconds();
+        Runnable task = () -> {
+            if (continuousRunIndex < numberOfRuns) {
+                try {
+                    String genBasePath = generateGarbage(options, continuousRunIndex, builder, startGenTimestamp);
+                    generatedGarbageBasePaths.add(genBasePath);
+                } catch (Exception e) {
+                    LOG.error("Error generating garbage in run " + continuousRunIndex, e);
+                }
+                LOG.info("Task executed. Count: " + (continuousRunIndex + 1));
+                continuousRunIndex++;
+            } else {
+                // Shutdown the executor once the task has run numberOfRuns times
+                continuousRunExecutor.shutdown();
+                LOG.info("Task completed " + numberOfRuns + " times. Stopping execution.");
+            }
+        };
+
+        // Schedule the task to run every intervalSeconds
+        continuousRunExecutor.scheduleAtFixedRate(task, 0, intervalSeconds, TimeUnit.SECONDS);
+
+        return generatedGarbageBasePaths;
+    }
+
+    /**
+     * Generate garbage nodes in the repository in order to allow for testing fullGC functionality.
+     *
+     * Returns the path of the generated FULLGC_GEN_BASE_PATH node (under the root).
+     * @param options
+     * @param closer
+     * @param runIndex
+     * @return
+     * @throws IOException
+     * @throws Exception
+     */
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long generationTimestamp = System.currentTimeMillis();
+
+        return generateGarbage(options, runIndex, builder, generationTimestamp);
+    }
+
+    private String generateGarbage(GenerateFullGCOptions options, int runIndex,
+                                   DocumentNodeStoreBuilder<?> builder, long timestamp) throws Exception {
+
+        if (builder == null) {
+            System.err.println("generateFullGC mode only available for DocumentNodeStore");
+            System.exit(1);
+        }
+
+        if (GenerateGarbageHelper.isInvalidGarbageGenerationMode(options.getGarbageType())) {
+            LOG.error("Invalid fullGCMode specified: " + options.getGarbageType() + " in: " + getClass().getName());
+            System.exit(1);
+        }
+
+        String generationBasePath = GEN_BASE_PATH + timestamp + "_" + runIndex;
+        documentNodeStore = builder.build();
+
+        NodeBuilder rootNode = documentNodeStore.getRoot().builder();
+
+        if (GenerateGarbageHelper.isEmptyProps(options.getGarbageType())) {
+            generateGarbageEmptyProps(rootNode, options, generationBasePath);
+        }
+        else if (GenerateGarbageHelper.isGapOrphans(options.getGarbageType())) {
+            generateGarbageGapOrphans(rootNode, options, generationBasePath);
+        }
+
+        return generationBasePath;
+    }
+    private void generateGarbageGapOrphans(NodeBuilder rootNode, GenerateFullGCOptions options, String generationBasePath)
+            throws CommitFailedException {
+
+        // validate gap orphans depth and level gap
+        if (options.getOrphansDepth() < 0 || options.getOrphansDepth() > GAP_ORPHANS_MAX_DEPTH) {
+            LOG.error("Invalid orphansDepth specified: " + options.getOrphansDepth() + " in: " + getClass().getName()
+                    + ". Must be an integer >= 0 and <= " + GAP_ORPHANS_MAX_DEPTH);
+            System.exit(1);
+        }
+        if (options.getOrphansLevelGap() < 0 || options.getOrphansLevelGap() > options.getOrphansDepth()) {
+            LOG.error("Invalid orphansLevelGap specified: " + options.getOrphansLevelGap() + " in: " + getClass().getName()
+                    + ". Must be a positive integer <= orphansDepth");
+            System.exit(1);
+        }
+
+        System.out.println("Generating fullGC gap orphans on the document: " + generationBasePath);
+
+        NodeBuilder garbageRootNode = rootNode.child(GARBAGE_GEN_ROOT_PATH_BASE).child(GARBAGE_GEN_ROOT_NODE_NAME);
+        garbageRootNode.child(generationBasePath).setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+
+        int nodesCountUnderParent = options.getCreateGarbageNodesCount() / options.getGarbageNodesParentCount();
+        for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+            // create parent node
+            garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).setProperty(JcrConstants.JCR_PRIMARYTYPE, "nt:folder", NAME);
+
+            // create child nodes under parent, according to gap orphans depth
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                getGapOrphanLeafGarbageNode(garbageRootNode, generationBasePath, options.getOrphansDepth(), i, j).
+                        setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+            }
+        }
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+
+        // Generate garbage nodes - GAP_ORPHANS - remove parent nodes
+        StringBuilder sbNodePath = new StringBuilder();
+        List<String> deleteNodePaths = new ArrayList<>();
+        for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+
+            // append the parent to the paths to delete
+            sbNodePath.setLength(0);
+            String path = getIdFromPath(
+                    sbNodePath.append("/").append(GARBAGE_GEN_ROOT_PATH).append("/").append(generationBasePath).append("/")
+                            .append(GEN_PARENT_NODE_PREFIX).append(i).toString());
+            deleteNodePaths.add(path);
+
+            // append all the gap orphans nodes between the parent and the level gap to the paths to delete
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                deleteNodePaths.addAll(generateGapOrphanNodePaths(generationBasePath, i, j, options.getOrphansDepth(), options.getOrphansLevelGap(), false));
+            }
+        }
+        // Remove all parent nodes
+        documentNodeStore.getDocumentStore().remove(org.apache.jackrabbit.oak.plugins.document.Collection.NODES, deleteNodePaths);
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+    }
+
+    private NodeBuilder getGapOrphanLeafGarbageNode(NodeBuilder garbageRootNode, String generationBasePath, int depth, int parentIndex, int nodeIndex) {
+        NodeBuilder garbageNode = garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + parentIndex);
+        for (int i = 1; i < depth; i++) {
+            garbageNode = garbageNode.child(GEN_NODE_PREFIX + nodeIndex + GEN_NODE_LEVEL_PREFIX + i);
+        }
+        // the leaf garbage node does not have level in its name
+        garbageNode = garbageNode.child(GEN_NODE_PREFIX + nodeIndex);
+        return garbageNode;
+    }
+
+    public static List<String> generateGapOrphanNodePaths(String generationBasePath, int parentIndex, int nodeIndex, int gapDepth) {
+        return generateGapOrphanNodePaths(generationBasePath, parentIndex, nodeIndex, gapDepth, 0, false);
+    }
+
+    /**
+     * Gets the paths of the nodes between the parent node and the level gap OR between the level gap and the leaf
+     * gap orphan node.
+     *
+     * Example 1: gapDepth = 4, gapLevelGap = 2, fromLevelGapToLeaf = false will return:
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2
+     * and will NOT return the rest of the nodes:
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3/Node_0 (the leaf node)
+     *
+     * Example 2: gapDepth = 5, gapLevelGap = 2, fromLevelGapToLeaf = true will return:
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3/Node_0_Level_4
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3/Node_0_Level_4/Node_0 (the leaf node)
+     * and will NOT return the nodes from the parent to the gap level:
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2
+     *
+     * @param generationBasePath
+     * @param parentIndex
+     * @param nodeIndex
+     * @param gapDepth - the depth in the tree until which to create gap orphan garbage nodes.
+     * @param gapLevelGap - the gap in the tree between the first gap orphan garbage node and its parent.
+     * @param fromLevelGapToLeaf - if true, the paths will be generated from the level gap to the leaf node, otherwise the paths will be generated from the parent node to the level gap.
+     * @return
+     */
+    public static List<String> generateGapOrphanNodePaths(String generationBasePath, int parentIndex, int nodeIndex, int gapDepth, int gapLevelGap, boolean fromLevelGapToLeaf) {
+        List<String> gapOrphanNodePaths = new ArrayList<>();
+
+        StringBuilder sbNodePath = new StringBuilder();
+        sbNodePath.setLength(0);
+
+        // start path from the parent node
+        sbNodePath.append("/").append(GARBAGE_GEN_ROOT_PATH).append("/").append(generationBasePath).append("/")
+                .append(GEN_PARENT_NODE_PREFIX).append(parentIndex);
+
+        for(int i = 1; i <= gapDepth; i ++) {
+            sbNodePath.append("/").append(GEN_NODE_PREFIX).append(nodeIndex);
+            // the leaf node does NOT have level in its name
+            if (i < gapDepth) {
+                sbNodePath.append(GEN_NODE_LEVEL_PREFIX).append(i);
+            }
+
+            // only append to list if the criteria for returning the node path is met
+            if ((!fromLevelGapToLeaf && i < gapLevelGap) || (fromLevelGapToLeaf && i >= gapLevelGap)) {
+                gapOrphanNodePaths.add(getIdFromPath(sbNodePath.toString()));
+            }
+        }
+
+        return gapOrphanNodePaths;
+    }
+
+    private void generateGarbageEmptyProps(NodeBuilder rootNode, GenerateFullGCOptions options, String generationBasePath) throws CommitFailedException {
+
+        System.out.println("Generating fullGC empty props on the document: " + generationBasePath);
+
+        NodeBuilder garbageRootNode = rootNode.child(GARBAGE_GEN_ROOT_PATH_BASE).child(GARBAGE_GEN_ROOT_NODE_NAME);
+        garbageRootNode.child(generationBasePath).setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+
+        int nodesCountUnderParent = options.getCreateGarbageNodesCount() / options.getGarbageNodesParentCount();
+        for(int i = 0; i < options.getGarbageNodesParentCount(); i ++) {
+            garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).setProperty(JcrConstants.JCR_PRIMARYTYPE, "nt:folder", NAME);
+
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                        setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+
+                if (GenerateGarbageHelper.isEmptyProps(options.getGarbageType())) {
+                    garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                            setProperty(EMPTY_PROPERTY_NAME, "bar", STRING);
+                }
+            }
+        }
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+
+
+        //2. Generate garbage nodes - EMPTY_PROPERTIES
+        for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+            for (int j = 0; j < nodesCountUnderParent; j++) {
+                garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                        removeProperty(EMPTY_PROPERTY_NAME);
+            }
+        }
+
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+    }
+
+    /**
+     * Cleans up all generated garbage by removing the node FULLGC_GEN_ROOT_PATH and all of
+     * its children (recursively)
+     * @param options
+     * @param closer
+     * @throws IOException
+     * @throws Exception
+     */
+    private void cleanGarbage(GenerateFullGCOptions options, Closer closer) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+
+        if (builder == null) {
+            System.err.println("generateFullGC mode only available for DocumentNodeStore");
+            System.exit(1);
+        }
+
+        System.out.println("Cleaning up all generated garbage:");
+        documentNodeStore = builder.build();
+
+        NodeBuilder rootBuilder = documentNodeStore.getRoot().builder();
+
+        NodeBuilder generatedGarbageRootBuilder = rootBuilder.child(GARBAGE_GEN_ROOT_PATH_BASE).child(GARBAGE_GEN_ROOT_NODE_NAME);
+
+        String garbageRootNodePath = "2:/"+ GARBAGE_GEN_ROOT_PATH;
+        List<String> childNodePaths = new ArrayList<>();
+        childNodePaths.add(garbageRootNodePath);
+
+        // get all paths of the tree nodes under the garbage root node
+        getTreeNodePaths(generatedGarbageRootBuilder, garbageRootNodePath, childNodePaths, 2);
+
+        documentNodeStore.getDocumentStore().remove(org.apache.jackrabbit.oak.plugins.document.Collection.NODES, childNodePaths);
+        documentNodeStore.merge(rootBuilder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+    }
+
+    /**
+     * Recursively get all paths of the tree nodes under the given root node.
+     * @param rootNode
+     * @param basePath
+     * @param treeNodePaths
+     * @param nodeLevel
+     */
+    private void getTreeNodePaths(NodeBuilder rootNode, String basePath,
+                                  List<String> treeNodePaths, int nodeLevel) {
+        String childBasePath = basePath.replaceFirst(nodeLevel + ":/", (nodeLevel + 1) + ":/");

Review Comment:
   This seems like a good candidate to make use of  `org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath` or `org.apache.jackrabbit.oak.plugins.document.Path.fromString`. Working on the Path object would also allow to verify that anything added to `treeNodePaths` is indeed at least 2 levels deep and is under the expected main generated-test-garbage path.



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateGarbageCommand.java:
##########
@@ -0,0 +1,553 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.commons.concurrent.ExecutorCloser;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.GenerateGarbageHelper;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateGarbageCommand implements Command, Closeable {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateGarbageCommand.class);
+
+    private final ScheduledExecutorService continuousRunExecutor = Executors.newScheduledThreadPool(1);
+
+    private static final String USAGE = "createGarbage {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * FullGC garbage should be generated under tmp node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH_BASE = "tmp";
+
+    /**
+     * The root node name for fullGC garbage generation, one level under tmp.
+     */
+    public static String GARBAGE_GEN_ROOT_NODE_NAME = "fullGC_GarbageRoot";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH = GARBAGE_GEN_ROOT_PATH_BASE + "/" + GARBAGE_GEN_ROOT_NODE_NAME;
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String GEN_BASE_PATH = "GenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String GEN_PARENT_NODE_PREFIX = "Parent_";
+    public static String GEN_NODE_PREFIX = "Node_";
+    public static String GEN_NODE_LEVEL_PREFIX = "_Level_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    /**
+     * The maximum depth in the tree at which to generate gap orphans garbage nodes.
+     * Used for validation of the orphansDepth / orphansLevelGap options.
+     */
+    private final int GAP_ORPHANS_MAX_DEPTH = 15;
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node FULLGC_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> orphansDepth;
+        final OptionSpec<Integer> orphansLevelGap;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public GenerateFullGCOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            orphansDepth = parser
+                    .accepts("orphansDepth", "the depth in the tree at which to create garbage nodes. Only applies to ORPHANS fullGC modes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            orphansLevelGap = parser
+                    .accepts("orphansLevelGap", "the gap in the tree between the first leaf gap orphan garbage node and its' parent. Only applies to ORPHANS fullGC modes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public GenerateFullGCOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getOrphansDepth() { return orphansDepth.value(options);}
+
+        public int getOrphansLevelGap() { return orphansLevelGap.value(options); }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {
+
+        try (Closer closer = Closer.create()) {
+            // Register the ExecutorService with Closer
+            closer.register(() -> {
+                // Properly shutdown the executor service
+                continuousRunExecutor.shutdown();
+                try {
+                    if (!continuousRunExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
+                        continuousRunExecutor.shutdownNow();
+                    }
+                } catch (InterruptedException e) {
+                    throw new RuntimeException(e);
+                }
+            });
+            execute(closer, args);
+        } catch (Throwable e) {
+            LOG.error("Command failed", e);
+            throw e;
+        }
+    }
+
+    /**
+     * Method with passed closer is necessary in order to allow for unit tests to check the output of the command.
+     * It is the responsibility of the caller to close the closer.
+     *
+     * Returns the list of generated garbage base paths (under the garbage root node).
+     * @param closer
+     * @param args
+     * @throws Exception
+     */
+    public List<String> execute(Closer closer, String... args) throws Exception {
+        continuousRunIndex = 0;
+
+        List<String> generateBasePaths = new ArrayList<>();
+
+        GenerateFullGCOptions options = new GenerateFullGCOptions(USAGE).parse(args);
+        String subCmd = options.getSubCmd();
+
+        if (GenerateFullGCOptions.CMD_GENERATE.equals(subCmd)) {
+
+            if (options.getCreateGarbageNodesCount() <= 0) {
+                LOG.error("Invalid garbageNodesCount specified: " + options.getCreateGarbageNodesCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+            if (options.getGarbageNodesParentCount() <= 0) {
+                LOG.error("Invalid garbageNodesParentCount specified: " + options.getGarbageNodesParentCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+
+            if (options.getNumberOfRuns() > 1 && options.getGenerateIntervalSeconds() > 0) {
+                generateBasePaths.addAll(generateGarbageContinuously(options, closer));
+            } else {
+                generateBasePaths.add(generateGarbage(options, closer, 0));
+            }
+        } else if (GenerateFullGCOptions.CMD_CLEAN.equals(subCmd)) {
+            cleanGarbage(options, closer);
+        } else {
+            System.err.println("Unknown revisions command: " + subCmd);
+        }
+
+        return generateBasePaths;
+    }
+
+    @Override
+    public void close() throws IOException {
+        new ExecutorCloser(this.continuousRunExecutor).close();
+    }
+
+    private List<String> generateGarbageContinuously(GenerateFullGCOptions options, Closer closer) throws Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long startGenTimestamp = System.currentTimeMillis();
+
+        List<String> generatedGarbageBasePaths = new ArrayList<>();
+
+        int numberOfRuns = options.getNumberOfRuns();
+        int intervalSeconds = options.getGenerateIntervalSeconds();
+        Runnable task = () -> {
+            if (continuousRunIndex < numberOfRuns) {
+                try {
+                    String genBasePath = generateGarbage(options, continuousRunIndex, builder, startGenTimestamp);
+                    generatedGarbageBasePaths.add(genBasePath);
+                } catch (Exception e) {
+                    LOG.error("Error generating garbage in run " + continuousRunIndex, e);
+                }
+                LOG.info("Task executed. Count: " + (continuousRunIndex + 1));
+                continuousRunIndex++;
+            } else {
+                // Shutdown the executor once the task has run numberOfRuns times
+                continuousRunExecutor.shutdown();
+                LOG.info("Task completed " + numberOfRuns + " times. Stopping execution.");
+            }
+        };
+
+        // Schedule the task to run every intervalSeconds
+        continuousRunExecutor.scheduleAtFixedRate(task, 0, intervalSeconds, TimeUnit.SECONDS);
+
+        return generatedGarbageBasePaths;
+    }
+
+    /**
+     * Generate garbage nodes in the repository in order to allow for testing fullGC functionality.
+     *
+     * Returns the path of the generated FULLGC_GEN_BASE_PATH node (under the root).
+     * @param options
+     * @param closer
+     * @param runIndex
+     * @return
+     * @throws IOException
+     * @throws Exception
+     */
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long generationTimestamp = System.currentTimeMillis();
+
+        return generateGarbage(options, runIndex, builder, generationTimestamp);
+    }
+
+    private String generateGarbage(GenerateFullGCOptions options, int runIndex,
+                                   DocumentNodeStoreBuilder<?> builder, long timestamp) throws Exception {
+
+        if (builder == null) {
+            System.err.println("generateFullGC mode only available for DocumentNodeStore");
+            System.exit(1);
+        }
+
+        if (GenerateGarbageHelper.isInvalidGarbageGenerationMode(options.getGarbageType())) {
+            LOG.error("Invalid fullGCMode specified: " + options.getGarbageType() + " in: " + getClass().getName());
+            System.exit(1);
+        }
+
+        String generationBasePath = GEN_BASE_PATH + timestamp + "_" + runIndex;
+        documentNodeStore = builder.build();
+
+        NodeBuilder rootNode = documentNodeStore.getRoot().builder();
+
+        if (GenerateGarbageHelper.isEmptyProps(options.getGarbageType())) {
+            generateGarbageEmptyProps(rootNode, options, generationBasePath);
+        }
+        else if (GenerateGarbageHelper.isGapOrphans(options.getGarbageType())) {
+            generateGarbageGapOrphans(rootNode, options, generationBasePath);
+        }
+
+        return generationBasePath;
+    }
+    private void generateGarbageGapOrphans(NodeBuilder rootNode, GenerateFullGCOptions options, String generationBasePath)
+            throws CommitFailedException {
+
+        // validate gap orphans depth and level gap
+        if (options.getOrphansDepth() < 0 || options.getOrphansDepth() > GAP_ORPHANS_MAX_DEPTH) {
+            LOG.error("Invalid orphansDepth specified: " + options.getOrphansDepth() + " in: " + getClass().getName()
+                    + ". Must be an integer >= 0 and <= " + GAP_ORPHANS_MAX_DEPTH);
+            System.exit(1);
+        }
+        if (options.getOrphansLevelGap() < 0 || options.getOrphansLevelGap() > options.getOrphansDepth()) {
+            LOG.error("Invalid orphansLevelGap specified: " + options.getOrphansLevelGap() + " in: " + getClass().getName()
+                    + ". Must be a positive integer <= orphansDepth");
+            System.exit(1);
+        }
+
+        System.out.println("Generating fullGC gap orphans on the document: " + generationBasePath);
+
+        NodeBuilder garbageRootNode = rootNode.child(GARBAGE_GEN_ROOT_PATH_BASE).child(GARBAGE_GEN_ROOT_NODE_NAME);
+        garbageRootNode.child(generationBasePath).setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+
+        int nodesCountUnderParent = options.getCreateGarbageNodesCount() / options.getGarbageNodesParentCount();
+        for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+            // create parent node
+            garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).setProperty(JcrConstants.JCR_PRIMARYTYPE, "nt:folder", NAME);
+
+            // create child nodes under parent, according to gap orphans depth
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                getGapOrphanLeafGarbageNode(garbageRootNode, generationBasePath, options.getOrphansDepth(), i, j).
+                        setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+            }
+        }
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+
+        // Generate garbage nodes - GAP_ORPHANS - remove parent nodes
+        StringBuilder sbNodePath = new StringBuilder();
+        List<String> deleteNodePaths = new ArrayList<>();
+        for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+
+            // append the parent to the paths to delete
+            sbNodePath.setLength(0);
+            String path = getIdFromPath(
+                    sbNodePath.append("/").append(GARBAGE_GEN_ROOT_PATH).append("/").append(generationBasePath).append("/")
+                            .append(GEN_PARENT_NODE_PREFIX).append(i).toString());
+            deleteNodePaths.add(path);
+
+            // append all the gap orphans nodes between the parent and the level gap to the paths to delete
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                deleteNodePaths.addAll(generateGapOrphanNodePaths(generationBasePath, i, j, options.getOrphansDepth(), options.getOrphansLevelGap(), false));
+            }
+        }
+        // Remove all parent nodes
+        documentNodeStore.getDocumentStore().remove(org.apache.jackrabbit.oak.plugins.document.Collection.NODES, deleteNodePaths);
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+    }
+
+    private NodeBuilder getGapOrphanLeafGarbageNode(NodeBuilder garbageRootNode, String generationBasePath, int depth, int parentIndex, int nodeIndex) {
+        NodeBuilder garbageNode = garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + parentIndex);
+        for (int i = 1; i < depth; i++) {
+            garbageNode = garbageNode.child(GEN_NODE_PREFIX + nodeIndex + GEN_NODE_LEVEL_PREFIX + i);
+        }
+        // the leaf garbage node does not have level in its name
+        garbageNode = garbageNode.child(GEN_NODE_PREFIX + nodeIndex);
+        return garbageNode;
+    }
+
+    public static List<String> generateGapOrphanNodePaths(String generationBasePath, int parentIndex, int nodeIndex, int gapDepth) {
+        return generateGapOrphanNodePaths(generationBasePath, parentIndex, nodeIndex, gapDepth, 0, false);
+    }
+
+    /**
+     * Gets the paths of the nodes between the parent node and the level gap OR between the level gap and the leaf
+     * gap orphan node.
+     *
+     * Example 1: gapDepth = 4, gapLevelGap = 2, fromLevelGapToLeaf = false will return:
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2
+     * and will NOT return the rest of the nodes:
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3/Node_0 (the leaf node)
+     *
+     * Example 2: gapDepth = 5, gapLevelGap = 2, fromLevelGapToLeaf = true will return:
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3/Node_0_Level_4
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3/Node_0_Level_4/Node_0 (the leaf node)
+     * and will NOT return the nodes from the parent to the gap level:
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2
+     *
+     * @param generationBasePath
+     * @param parentIndex
+     * @param nodeIndex
+     * @param gapDepth - the depth in the tree until which to create gap orphan garbage nodes.
+     * @param gapLevelGap - the gap in the tree between the first gap orphan garbage node and its parent.
+     * @param fromLevelGapToLeaf - if true, the paths will be generated from the level gap to the leaf node, otherwise the paths will be generated from the parent node to the level gap.
+     * @return
+     */
+    public static List<String> generateGapOrphanNodePaths(String generationBasePath, int parentIndex, int nodeIndex, int gapDepth, int gapLevelGap, boolean fromLevelGapToLeaf) {
+        List<String> gapOrphanNodePaths = new ArrayList<>();
+
+        StringBuilder sbNodePath = new StringBuilder();
+        sbNodePath.setLength(0);
+
+        // start path from the parent node
+        sbNodePath.append("/").append(GARBAGE_GEN_ROOT_PATH).append("/").append(generationBasePath).append("/")
+                .append(GEN_PARENT_NODE_PREFIX).append(parentIndex);
+
+        for(int i = 1; i <= gapDepth; i ++) {
+            sbNodePath.append("/").append(GEN_NODE_PREFIX).append(nodeIndex);
+            // the leaf node does NOT have level in its name
+            if (i < gapDepth) {
+                sbNodePath.append(GEN_NODE_LEVEL_PREFIX).append(i);
+            }
+
+            // only append to list if the criteria for returning the node path is met
+            if ((!fromLevelGapToLeaf && i < gapLevelGap) || (fromLevelGapToLeaf && i >= gapLevelGap)) {
+                gapOrphanNodePaths.add(getIdFromPath(sbNodePath.toString()));
+            }
+        }
+
+        return gapOrphanNodePaths;
+    }
+
+    private void generateGarbageEmptyProps(NodeBuilder rootNode, GenerateFullGCOptions options, String generationBasePath) throws CommitFailedException {
+
+        System.out.println("Generating fullGC empty props on the document: " + generationBasePath);
+
+        NodeBuilder garbageRootNode = rootNode.child(GARBAGE_GEN_ROOT_PATH_BASE).child(GARBAGE_GEN_ROOT_NODE_NAME);
+        garbageRootNode.child(generationBasePath).setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+
+        int nodesCountUnderParent = options.getCreateGarbageNodesCount() / options.getGarbageNodesParentCount();
+        for(int i = 0; i < options.getGarbageNodesParentCount(); i ++) {
+            garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).setProperty(JcrConstants.JCR_PRIMARYTYPE, "nt:folder", NAME);
+
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                        setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+
+                if (GenerateGarbageHelper.isEmptyProps(options.getGarbageType())) {
+                    garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                            setProperty(EMPTY_PROPERTY_NAME, "bar", STRING);
+                }
+            }
+        }
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+
+
+        //2. Generate garbage nodes - EMPTY_PROPERTIES
+        for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+            for (int j = 0; j < nodesCountUnderParent; j++) {
+                garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                        removeProperty(EMPTY_PROPERTY_NAME);
+            }
+        }
+
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();

Review Comment:
   this shouldn't be necessary - automatically happens in the background and at dispose time.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "shodaaan (via GitHub)" <gi...@apache.org>.
shodaaan commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1812633543


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateGarbageCommand.java:
##########
@@ -0,0 +1,427 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.commons.concurrent.ExecutorCloser;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.GenerateGarbageHelper;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateGarbageCommand implements Command, Closeable {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateGarbageCommand.class);
+
+    private final ScheduledExecutorService continuousRunExecutor = Executors.newScheduledThreadPool(1);
+
+    private static final String USAGE = "createGarbage {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * FullGC garbage should be generated under tmp node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH_BASE = "tmp";
+
+    /**
+     * The root node name for fullGC garbage generation, one level under tmp.
+     */
+    public static String GARBAGE_GEN_ROOT_NODE_NAME = "fullGC_GarbageRoot";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH = GARBAGE_GEN_ROOT_PATH_BASE + "/" + GARBAGE_GEN_ROOT_NODE_NAME;
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String GEN_BASE_PATH = "GenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String GEN_PARENT_NODE_PREFIX = "Parent_";
+    public static String GEN_NODE_PREFIX = "Node_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node FULLGC_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public GenerateFullGCOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public GenerateFullGCOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {
+
+        try (Closer closer = Closer.create()) {
+            execute(closer, args);
+        } catch (Throwable e) {
+            LOG.error("Command failed", e);
+            throw e;
+        }
+    }
+
+    /**
+     * Method with passed closer is necessary in order to allow for unit tests to check the output of the command.
+     * It is the responsibility of the caller to close the closer.
+     *
+     * Returns the list of generated garbage base paths (under the garbage root node).
+     * @param closer
+     * @param args
+     * @throws Exception
+     */
+    public List<String> execute(Closer closer, String... args) throws Exception {
+        continuousRunIndex = 0;
+
+        List<String> generateBasePaths = new ArrayList<>();
+
+        GenerateFullGCOptions options = new GenerateFullGCOptions(USAGE).parse(args);
+        String subCmd = options.getSubCmd();
+
+        if (GenerateFullGCOptions.CMD_GENERATE.equals(subCmd)) {
+
+            if (options.getCreateGarbageNodesCount() <= 0) {
+                LOG.error("Invalid garbageNodesCount specified: " + options.getCreateGarbageNodesCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+            if (options.getGarbageNodesParentCount() <= 0) {
+                LOG.error("Invalid garbageNodesParentCount specified: " + options.getGarbageNodesParentCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+
+            if (options.getNumberOfRuns() > 1 && options.getGenerateIntervalSeconds() > 0) {
+                generateBasePaths.addAll(generateGarbageContinuously(options, closer));
+            } else {
+                generateBasePaths.add(generateGarbage(options, closer, 0));
+            }
+        } else if (GenerateFullGCOptions.CMD_CLEAN.equals(subCmd)) {
+            cleanGarbage(options, closer);
+        } else {
+            System.err.println("Unknown revisions command: " + subCmd);
+        }
+
+        return generateBasePaths;
+    }
+
+    @Override
+    public void close() throws IOException {
+        new ExecutorCloser(this.continuousRunExecutor).close();
+    }
+
+    private List<String> generateGarbageContinuously(GenerateFullGCOptions options, Closer closer) throws Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long startGenTimestamp = System.currentTimeMillis();
+
+        List<String> generatedGarbageBasePaths = new ArrayList<>();
+
+        int numberOfRuns = options.getNumberOfRuns();
+        int intervalSeconds = options.getGenerateIntervalSeconds();
+        Runnable task = () -> {
+            if (continuousRunIndex < numberOfRuns) {
+                try {
+                    String genBasePath = generateGarbage(options, closer, continuousRunIndex, builder, startGenTimestamp);
+                    generatedGarbageBasePaths.add(genBasePath);
+                } catch (Exception e) {
+                    LOG.error("Error generating garbage in run " + continuousRunIndex, e);
+                }
+                LOG.info("Task executed. Count: " + (continuousRunIndex + 1));
+                continuousRunIndex++;
+            } else {
+                // Shutdown the executor once the task has run numberOfRuns times
+                continuousRunExecutor.shutdown();
+                LOG.info("Task completed " + numberOfRuns + " times. Stopping execution.");
+            }
+        };
+
+        // Schedule the task to run every intervalSeconds
+        continuousRunExecutor.scheduleAtFixedRate(task, 0, intervalSeconds, TimeUnit.SECONDS);
+
+        return generatedGarbageBasePaths;
+    }
+
+    /**
+     * Generate garbage nodes in the repository in order to allow for testing fullGC functionality.
+     *
+     * Returns the path of the generated FULLGC_GEN_BASE_PATH node (under the root).
+     * @param options
+     * @param closer
+     * @param runIndex
+     * @return
+     * @throws IOException
+     * @throws Exception
+     */
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long generationTimestamp = System.currentTimeMillis();
+
+        return generateGarbage(options, closer, runIndex, builder, generationTimestamp);
+    }
+
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex,

Review Comment:
   Fixed, thank you for the suggestion.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "Joscorbe (via GitHub)" <gi...@apache.org>.
Joscorbe commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1813522487


##########
oak-run/src/test/java/org/apache/jackrabbit/oak/plugins/document/GenerateFullGCCommandTest.java:
##########
@@ -0,0 +1,293 @@
+/*
+ * 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 org.apache.jackrabbit.guava.common.collect.ImmutableList;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.plugins.document.util.MongoConnection;
+import org.apache.jackrabbit.oak.run.GenerateFullGCCommand;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.apache.jackrabbit.oak.plugins.document.CommandTestUtils.captureSystemOut;
+import static org.apache.jackrabbit.oak.run.GenerateFullGCCommand.EMPTY_PROPERTY_NAME;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+public class GenerateFullGCCommandTest {
+
+    final String OPTION_GARBAGE_NODES_COUNT = "--garbageNodesCount";
+    final String OPTION_GARBAGE_NODES_PARENT_COUNT = "--garbageNodesParentCount";
+    final String OPTION_GARBAGE_TYPE = "--garbageType";
+    final String OPTION_NUMBER_OF_RUNS = "--numberOfRuns";
+    final String OPTION_GENERATE_INTERVAL_SECONDS = "--generateIntervalSeconds";
+
+    @Rule
+    public MongoConnectionFactory connectionFactory = new MongoConnectionFactory();
+
+    @Rule
+    public DocumentMKBuilderProvider builderProvider = new DocumentMKBuilderProvider();
+
+    private DocumentNodeStore ns;
+
+    private Closer closer;
+
+    @BeforeClass
+    public static void assumeMongoDB() {
+        assumeTrue(MongoUtils.isAvailable());
+    }
+
+    @Before
+    public void before() {
+        ns = createDocumentNodeStore();
+    }
+
+    @After
+    public void after() {
+        try {
+            closer.close();
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+        ns.dispose();
+    }
+
+    private DocumentNodeStore createDocumentNodeStore() {
+        MongoConnection c = connectionFactory.getConnection();
+        assertNotNull(c);
+        MongoUtils.dropCollections(c.getDatabase());
+        return builderProvider.newBuilder().setFullGCEnabled(false).setLeaseCheckMode(LeaseCheckMode.DISABLED).setAsyncDelay(0)
+                .setMongoDB(c.getMongoClient(), c.getDBName()).getNodeStore();
+    }
+
+    private List<String> getChildNodeNames(NodeState node) {
+        List<String> childNodeNames = new ArrayList<>();
+        for (String childNodeName : node.getChildNodeNames()) {
+            childNodeNames.add(childNodeName);
+        }
+        return childNodeNames;
+    }
+
+    @Ignore
+    @Test
+    public void generateGarbageEmptyPropsUnderOneParentOnCustomMongoDB() {
+        ns.dispose();
+
+        // this should be a valid mongoDB connection string for the MongoDB on which to generate the garbage
+        String mongoDBConnString = "";
+        GenerateFullGCCmd cmd = new GenerateFullGCCmd( mongoDBConnString, OPTION_GARBAGE_NODES_COUNT, "10", OPTION_GARBAGE_NODES_PARENT_COUNT, "1",
+                OPTION_GARBAGE_TYPE, "1");
+        String output = captureSystemOut(cmd);
+
+        //sleep thread for 10 seconds to allow the garbage generation to complete
+        try {
+            Thread.sleep(10000);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+
+        DocumentNodeStore nodeStore = cmd.getCommand().getDocumentNodeStore();
+        NodeState garbageRootNodeState = nodeStore.getRoot().getChildNode(GenerateFullGCCommand.FULLGC_GEN_ROOT_PATH);
+        List<String> garbageRootNodeChildNames = getChildNodeNames(garbageRootNodeState);
+
+        assertEquals(garbageRootNodeChildNames.size(), 1);
+        List<String> propParentsGarbageNodeNames = getChildNodeNames(garbageRootNodeState.getChildNode(garbageRootNodeChildNames.get(0)));
+        assertEquals(propParentsGarbageNodeNames.size(), 1);
+
+        List<String> propGarbageNodeNames = getChildNodeNames(garbageRootNodeState.getChildNode(garbageRootNodeChildNames.get(0)).getChildNode(propParentsGarbageNodeNames.get(0)));
+        assertEquals(propGarbageNodeNames.size(), 10);
+    }
+
+    @Test
+    public void cleanupAllGarbage() {
+
+        // generate garbage first
+        GenerateFullGCCmd cmdGenerateGarbage = new GenerateFullGCCmd(OPTION_GARBAGE_NODES_COUNT, "10", OPTION_GARBAGE_NODES_PARENT_COUNT, "1",
+                OPTION_GARBAGE_TYPE, "2");
+        cmdGenerateGarbage.run();
+        Closer cmd1Closer = cmdGenerateGarbage.getCloser();
+
+        // cleanup garbage
+        String mongoDBConnString = "mongodb://cm-p107053-e251100-username:3fUEP9UHPD9yP4H@cmp107053e251100r355e01-shard-00-00.wxmqq.mongodb.net:27017,cmp107053e251100r355e01-shard-00-01.wxmqq.mongodb.net:27017,cmp107053e251100r355e01-shard-00-02.wxmqq.mongodb.net:27017/cm-p107053-e251100-database?ssl=true&authSource=admin&replicaSet=atlas-4x602o-shard-0&retryWrites=true&readPreference=primaryPreferred&readConcernLevel=majority&w=majority";

Review Comment:
   Check this please!



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "shodaaan (via GitHub)" <gi...@apache.org>.
shodaaan commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1820293664


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateGarbageCommand.java:
##########
@@ -0,0 +1,553 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.commons.concurrent.ExecutorCloser;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.GenerateGarbageHelper;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateGarbageCommand implements Command, Closeable {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateGarbageCommand.class);
+
+    private final ScheduledExecutorService continuousRunExecutor = Executors.newScheduledThreadPool(1);
+
+    private static final String USAGE = "createGarbage {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * FullGC garbage should be generated under tmp node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH_BASE = "tmp";
+
+    /**
+     * The root node name for fullGC garbage generation, one level under tmp.
+     */
+    public static String GARBAGE_GEN_ROOT_NODE_NAME = "fullGC_GarbageRoot";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH = GARBAGE_GEN_ROOT_PATH_BASE + "/" + GARBAGE_GEN_ROOT_NODE_NAME;
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String GEN_BASE_PATH = "GenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String GEN_PARENT_NODE_PREFIX = "Parent_";
+    public static String GEN_NODE_PREFIX = "Node_";
+    public static String GEN_NODE_LEVEL_PREFIX = "_Level_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    /**
+     * The maximum depth in the tree at which to generate gap orphans garbage nodes.
+     * Used for validation of the orphansDepth / orphansLevelGap options.
+     */
+    private final int GAP_ORPHANS_MAX_DEPTH = 15;
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node FULLGC_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> orphansDepth;
+        final OptionSpec<Integer> orphansLevelGap;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public GenerateFullGCOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            orphansDepth = parser
+                    .accepts("orphansDepth", "the depth in the tree at which to create garbage nodes. Only applies to ORPHANS fullGC modes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            orphansLevelGap = parser
+                    .accepts("orphansLevelGap", "the gap in the tree between the first leaf gap orphan garbage node and its' parent. Only applies to ORPHANS fullGC modes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public GenerateFullGCOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getOrphansDepth() { return orphansDepth.value(options);}
+
+        public int getOrphansLevelGap() { return orphansLevelGap.value(options); }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {
+
+        try (Closer closer = Closer.create()) {
+            // Register the ExecutorService with Closer
+            closer.register(() -> {
+                // Properly shutdown the executor service
+                continuousRunExecutor.shutdown();
+                try {
+                    if (!continuousRunExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
+                        continuousRunExecutor.shutdownNow();
+                    }
+                } catch (InterruptedException e) {
+                    throw new RuntimeException(e);
+                }
+            });
+            execute(closer, args);
+        } catch (Throwable e) {
+            LOG.error("Command failed", e);
+            throw e;
+        }
+    }
+
+    /**
+     * Method with passed closer is necessary in order to allow for unit tests to check the output of the command.
+     * It is the responsibility of the caller to close the closer.
+     *
+     * Returns the list of generated garbage base paths (under the garbage root node).
+     * @param closer
+     * @param args
+     * @throws Exception
+     */
+    public List<String> execute(Closer closer, String... args) throws Exception {
+        continuousRunIndex = 0;
+
+        List<String> generateBasePaths = new ArrayList<>();
+
+        GenerateFullGCOptions options = new GenerateFullGCOptions(USAGE).parse(args);
+        String subCmd = options.getSubCmd();
+
+        if (GenerateFullGCOptions.CMD_GENERATE.equals(subCmd)) {
+
+            if (options.getCreateGarbageNodesCount() <= 0) {
+                LOG.error("Invalid garbageNodesCount specified: " + options.getCreateGarbageNodesCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+            if (options.getGarbageNodesParentCount() <= 0) {
+                LOG.error("Invalid garbageNodesParentCount specified: " + options.getGarbageNodesParentCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+
+            if (options.getNumberOfRuns() > 1 && options.getGenerateIntervalSeconds() > 0) {
+                generateBasePaths.addAll(generateGarbageContinuously(options, closer));
+            } else {
+                generateBasePaths.add(generateGarbage(options, closer, 0));
+            }
+        } else if (GenerateFullGCOptions.CMD_CLEAN.equals(subCmd)) {
+            cleanGarbage(options, closer);
+        } else {
+            System.err.println("Unknown revisions command: " + subCmd);
+        }
+
+        return generateBasePaths;
+    }
+
+    @Override
+    public void close() throws IOException {
+        new ExecutorCloser(this.continuousRunExecutor).close();
+    }
+
+    private List<String> generateGarbageContinuously(GenerateFullGCOptions options, Closer closer) throws Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long startGenTimestamp = System.currentTimeMillis();
+
+        List<String> generatedGarbageBasePaths = new ArrayList<>();
+
+        int numberOfRuns = options.getNumberOfRuns();
+        int intervalSeconds = options.getGenerateIntervalSeconds();
+        Runnable task = () -> {
+            if (continuousRunIndex < numberOfRuns) {
+                try {
+                    String genBasePath = generateGarbage(options, continuousRunIndex, builder, startGenTimestamp);
+                    generatedGarbageBasePaths.add(genBasePath);
+                } catch (Exception e) {
+                    LOG.error("Error generating garbage in run " + continuousRunIndex, e);
+                }
+                LOG.info("Task executed. Count: " + (continuousRunIndex + 1));
+                continuousRunIndex++;
+            } else {
+                // Shutdown the executor once the task has run numberOfRuns times
+                continuousRunExecutor.shutdown();
+                LOG.info("Task completed " + numberOfRuns + " times. Stopping execution.");
+            }
+        };
+
+        // Schedule the task to run every intervalSeconds
+        continuousRunExecutor.scheduleAtFixedRate(task, 0, intervalSeconds, TimeUnit.SECONDS);
+
+        return generatedGarbageBasePaths;
+    }
+
+    /**
+     * Generate garbage nodes in the repository in order to allow for testing fullGC functionality.
+     *
+     * Returns the path of the generated FULLGC_GEN_BASE_PATH node (under the root).
+     * @param options
+     * @param closer
+     * @param runIndex
+     * @return
+     * @throws IOException
+     * @throws Exception
+     */
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long generationTimestamp = System.currentTimeMillis();
+
+        return generateGarbage(options, runIndex, builder, generationTimestamp);
+    }
+
+    private String generateGarbage(GenerateFullGCOptions options, int runIndex,
+                                   DocumentNodeStoreBuilder<?> builder, long timestamp) throws Exception {
+
+        if (builder == null) {
+            System.err.println("generateFullGC mode only available for DocumentNodeStore");
+            System.exit(1);
+        }
+
+        if (GenerateGarbageHelper.isInvalidGarbageGenerationMode(options.getGarbageType())) {
+            LOG.error("Invalid fullGCMode specified: " + options.getGarbageType() + " in: " + getClass().getName());
+            System.exit(1);
+        }
+
+        String generationBasePath = GEN_BASE_PATH + timestamp + "_" + runIndex;
+        documentNodeStore = builder.build();
+
+        NodeBuilder rootNode = documentNodeStore.getRoot().builder();
+
+        if (GenerateGarbageHelper.isEmptyProps(options.getGarbageType())) {
+            generateGarbageEmptyProps(rootNode, options, generationBasePath);
+        }
+        else if (GenerateGarbageHelper.isGapOrphans(options.getGarbageType())) {
+            generateGarbageGapOrphans(rootNode, options, generationBasePath);
+        }
+
+        return generationBasePath;
+    }
+    private void generateGarbageGapOrphans(NodeBuilder rootNode, GenerateFullGCOptions options, String generationBasePath)
+            throws CommitFailedException {
+
+        // validate gap orphans depth and level gap
+        if (options.getOrphansDepth() < 0 || options.getOrphansDepth() > GAP_ORPHANS_MAX_DEPTH) {
+            LOG.error("Invalid orphansDepth specified: " + options.getOrphansDepth() + " in: " + getClass().getName()
+                    + ". Must be an integer >= 0 and <= " + GAP_ORPHANS_MAX_DEPTH);
+            System.exit(1);
+        }
+        if (options.getOrphansLevelGap() < 0 || options.getOrphansLevelGap() > options.getOrphansDepth()) {
+            LOG.error("Invalid orphansLevelGap specified: " + options.getOrphansLevelGap() + " in: " + getClass().getName()
+                    + ". Must be a positive integer <= orphansDepth");
+            System.exit(1);
+        }
+
+        System.out.println("Generating fullGC gap orphans on the document: " + generationBasePath);
+
+        NodeBuilder garbageRootNode = rootNode.child(GARBAGE_GEN_ROOT_PATH_BASE).child(GARBAGE_GEN_ROOT_NODE_NAME);
+        garbageRootNode.child(generationBasePath).setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+
+        int nodesCountUnderParent = options.getCreateGarbageNodesCount() / options.getGarbageNodesParentCount();
+        for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+            // create parent node
+            garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).setProperty(JcrConstants.JCR_PRIMARYTYPE, "nt:folder", NAME);
+
+            // create child nodes under parent, according to gap orphans depth
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                getGapOrphanLeafGarbageNode(garbageRootNode, generationBasePath, options.getOrphansDepth(), i, j).
+                        setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+            }
+        }
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+
+        // Generate garbage nodes - GAP_ORPHANS - remove parent nodes
+        StringBuilder sbNodePath = new StringBuilder();
+        List<String> deleteNodePaths = new ArrayList<>();
+        for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+
+            // append the parent to the paths to delete
+            sbNodePath.setLength(0);
+            String path = getIdFromPath(
+                    sbNodePath.append("/").append(GARBAGE_GEN_ROOT_PATH).append("/").append(generationBasePath).append("/")
+                            .append(GEN_PARENT_NODE_PREFIX).append(i).toString());
+            deleteNodePaths.add(path);
+
+            // append all the gap orphans nodes between the parent and the level gap to the paths to delete
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                deleteNodePaths.addAll(generateGapOrphanNodePaths(generationBasePath, i, j, options.getOrphansDepth(), options.getOrphansLevelGap(), false));
+            }
+        }
+        // Remove all parent nodes
+        documentNodeStore.getDocumentStore().remove(org.apache.jackrabbit.oak.plugins.document.Collection.NODES, deleteNodePaths);
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+    }
+
+    private NodeBuilder getGapOrphanLeafGarbageNode(NodeBuilder garbageRootNode, String generationBasePath, int depth, int parentIndex, int nodeIndex) {
+        NodeBuilder garbageNode = garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + parentIndex);
+        for (int i = 1; i < depth; i++) {
+            garbageNode = garbageNode.child(GEN_NODE_PREFIX + nodeIndex + GEN_NODE_LEVEL_PREFIX + i);
+        }
+        // the leaf garbage node does not have level in its name
+        garbageNode = garbageNode.child(GEN_NODE_PREFIX + nodeIndex);
+        return garbageNode;
+    }
+
+    public static List<String> generateGapOrphanNodePaths(String generationBasePath, int parentIndex, int nodeIndex, int gapDepth) {
+        return generateGapOrphanNodePaths(generationBasePath, parentIndex, nodeIndex, gapDepth, 0, false);
+    }
+
+    /**
+     * Gets the paths of the nodes between the parent node and the level gap OR between the level gap and the leaf
+     * gap orphan node.
+     *
+     * Example 1: gapDepth = 4, gapLevelGap = 2, fromLevelGapToLeaf = false will return:
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2
+     * and will NOT return the rest of the nodes:
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3/Node_0 (the leaf node)
+     *
+     * Example 2: gapDepth = 5, gapLevelGap = 2, fromLevelGapToLeaf = true will return:
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3/Node_0_Level_4
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3/Node_0_Level_4/Node_0 (the leaf node)
+     * and will NOT return the nodes from the parent to the gap level:
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2
+     *
+     * @param generationBasePath
+     * @param parentIndex
+     * @param nodeIndex
+     * @param gapDepth - the depth in the tree until which to create gap orphan garbage nodes.
+     * @param gapLevelGap - the gap in the tree between the first gap orphan garbage node and its parent.
+     * @param fromLevelGapToLeaf - if true, the paths will be generated from the level gap to the leaf node, otherwise the paths will be generated from the parent node to the level gap.
+     * @return
+     */
+    public static List<String> generateGapOrphanNodePaths(String generationBasePath, int parentIndex, int nodeIndex, int gapDepth, int gapLevelGap, boolean fromLevelGapToLeaf) {
+        List<String> gapOrphanNodePaths = new ArrayList<>();
+
+        StringBuilder sbNodePath = new StringBuilder();
+        sbNodePath.setLength(0);
+
+        // start path from the parent node
+        sbNodePath.append("/").append(GARBAGE_GEN_ROOT_PATH).append("/").append(generationBasePath).append("/")
+                .append(GEN_PARENT_NODE_PREFIX).append(parentIndex);
+
+        for(int i = 1; i <= gapDepth; i ++) {
+            sbNodePath.append("/").append(GEN_NODE_PREFIX).append(nodeIndex);
+            // the leaf node does NOT have level in its name
+            if (i < gapDepth) {
+                sbNodePath.append(GEN_NODE_LEVEL_PREFIX).append(i);
+            }
+
+            // only append to list if the criteria for returning the node path is met
+            if ((!fromLevelGapToLeaf && i < gapLevelGap) || (fromLevelGapToLeaf && i >= gapLevelGap)) {
+                gapOrphanNodePaths.add(getIdFromPath(sbNodePath.toString()));
+            }
+        }
+
+        return gapOrphanNodePaths;
+    }
+
+    private void generateGarbageEmptyProps(NodeBuilder rootNode, GenerateFullGCOptions options, String generationBasePath) throws CommitFailedException {
+
+        System.out.println("Generating fullGC empty props on the document: " + generationBasePath);
+
+        NodeBuilder garbageRootNode = rootNode.child(GARBAGE_GEN_ROOT_PATH_BASE).child(GARBAGE_GEN_ROOT_NODE_NAME);
+        garbageRootNode.child(generationBasePath).setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+
+        int nodesCountUnderParent = options.getCreateGarbageNodesCount() / options.getGarbageNodesParentCount();
+        for(int i = 0; i < options.getGarbageNodesParentCount(); i ++) {
+            garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).setProperty(JcrConstants.JCR_PRIMARYTYPE, "nt:folder", NAME);
+
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                        setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+
+                if (GenerateGarbageHelper.isEmptyProps(options.getGarbageType())) {
+                    garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                            setProperty(EMPTY_PROPERTY_NAME, "bar", STRING);
+                }
+            }
+        }
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+
+
+        //2. Generate garbage nodes - EMPTY_PROPERTIES
+        for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+            for (int j = 0; j < nodesCountUnderParent; j++) {
+                garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                        removeProperty(EMPTY_PROPERTY_NAME);
+            }
+        }
+
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+    }
+
+    /**
+     * Cleans up all generated garbage by removing the node FULLGC_GEN_ROOT_PATH and all of
+     * its children (recursively)
+     * @param options
+     * @param closer
+     * @throws IOException
+     * @throws Exception
+     */
+    private void cleanGarbage(GenerateFullGCOptions options, Closer closer) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+
+        if (builder == null) {
+            System.err.println("generateFullGC mode only available for DocumentNodeStore");
+            System.exit(1);
+        }
+
+        System.out.println("Cleaning up all generated garbage:");
+        documentNodeStore = builder.build();
+
+        NodeBuilder rootBuilder = documentNodeStore.getRoot().builder();
+
+        NodeBuilder generatedGarbageRootBuilder = rootBuilder.child(GARBAGE_GEN_ROOT_PATH_BASE).child(GARBAGE_GEN_ROOT_NODE_NAME);
+
+        String garbageRootNodePath = "2:/"+ GARBAGE_GEN_ROOT_PATH;
+        List<String> childNodePaths = new ArrayList<>();
+        childNodePaths.add(garbageRootNodePath);
+
+        // get all paths of the tree nodes under the garbage root node
+        getTreeNodePaths(generatedGarbageRootBuilder, garbageRootNodePath, childNodePaths, 2);
+
+        documentNodeStore.getDocumentStore().remove(org.apache.jackrabbit.oak.plugins.document.Collection.NODES, childNodePaths);
+        documentNodeStore.merge(rootBuilder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+    }
+
+    /**
+     * Recursively get all paths of the tree nodes under the given root node.
+     * @param rootNode
+     * @param basePath
+     * @param treeNodePaths
+     * @param nodeLevel
+     */
+    private void getTreeNodePaths(NodeBuilder rootNode, String basePath,
+                                  List<String> treeNodePaths, int nodeLevel) {
+        String childBasePath = basePath.replaceFirst(nodeLevel + ":/", (nodeLevel + 1) + ":/");

Review Comment:
   Changed, thank you for the suggestion.



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateGarbageCommand.java:
##########
@@ -0,0 +1,553 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.commons.concurrent.ExecutorCloser;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.GenerateGarbageHelper;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateGarbageCommand implements Command, Closeable {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateGarbageCommand.class);
+
+    private final ScheduledExecutorService continuousRunExecutor = Executors.newScheduledThreadPool(1);
+
+    private static final String USAGE = "createGarbage {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * FullGC garbage should be generated under tmp node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH_BASE = "tmp";
+
+    /**
+     * The root node name for fullGC garbage generation, one level under tmp.
+     */
+    public static String GARBAGE_GEN_ROOT_NODE_NAME = "fullGC_GarbageRoot";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH = GARBAGE_GEN_ROOT_PATH_BASE + "/" + GARBAGE_GEN_ROOT_NODE_NAME;
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String GEN_BASE_PATH = "GenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String GEN_PARENT_NODE_PREFIX = "Parent_";
+    public static String GEN_NODE_PREFIX = "Node_";
+    public static String GEN_NODE_LEVEL_PREFIX = "_Level_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    /**
+     * The maximum depth in the tree at which to generate gap orphans garbage nodes.
+     * Used for validation of the orphansDepth / orphansLevelGap options.
+     */
+    private final int GAP_ORPHANS_MAX_DEPTH = 15;
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node FULLGC_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> orphansDepth;
+        final OptionSpec<Integer> orphansLevelGap;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public GenerateFullGCOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            orphansDepth = parser
+                    .accepts("orphansDepth", "the depth in the tree at which to create garbage nodes. Only applies to ORPHANS fullGC modes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            orphansLevelGap = parser
+                    .accepts("orphansLevelGap", "the gap in the tree between the first leaf gap orphan garbage node and its' parent. Only applies to ORPHANS fullGC modes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public GenerateFullGCOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getOrphansDepth() { return orphansDepth.value(options);}
+
+        public int getOrphansLevelGap() { return orphansLevelGap.value(options); }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {
+
+        try (Closer closer = Closer.create()) {
+            // Register the ExecutorService with Closer
+            closer.register(() -> {
+                // Properly shutdown the executor service
+                continuousRunExecutor.shutdown();
+                try {
+                    if (!continuousRunExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
+                        continuousRunExecutor.shutdownNow();
+                    }
+                } catch (InterruptedException e) {
+                    throw new RuntimeException(e);
+                }
+            });
+            execute(closer, args);
+        } catch (Throwable e) {
+            LOG.error("Command failed", e);
+            throw e;
+        }
+    }
+
+    /**
+     * Method with passed closer is necessary in order to allow for unit tests to check the output of the command.
+     * It is the responsibility of the caller to close the closer.
+     *
+     * Returns the list of generated garbage base paths (under the garbage root node).
+     * @param closer
+     * @param args
+     * @throws Exception
+     */
+    public List<String> execute(Closer closer, String... args) throws Exception {
+        continuousRunIndex = 0;
+
+        List<String> generateBasePaths = new ArrayList<>();
+
+        GenerateFullGCOptions options = new GenerateFullGCOptions(USAGE).parse(args);
+        String subCmd = options.getSubCmd();
+
+        if (GenerateFullGCOptions.CMD_GENERATE.equals(subCmd)) {
+
+            if (options.getCreateGarbageNodesCount() <= 0) {
+                LOG.error("Invalid garbageNodesCount specified: " + options.getCreateGarbageNodesCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+            if (options.getGarbageNodesParentCount() <= 0) {
+                LOG.error("Invalid garbageNodesParentCount specified: " + options.getGarbageNodesParentCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+
+            if (options.getNumberOfRuns() > 1 && options.getGenerateIntervalSeconds() > 0) {
+                generateBasePaths.addAll(generateGarbageContinuously(options, closer));
+            } else {
+                generateBasePaths.add(generateGarbage(options, closer, 0));
+            }
+        } else if (GenerateFullGCOptions.CMD_CLEAN.equals(subCmd)) {
+            cleanGarbage(options, closer);
+        } else {
+            System.err.println("Unknown revisions command: " + subCmd);
+        }
+
+        return generateBasePaths;
+    }
+
+    @Override
+    public void close() throws IOException {
+        new ExecutorCloser(this.continuousRunExecutor).close();
+    }
+
+    private List<String> generateGarbageContinuously(GenerateFullGCOptions options, Closer closer) throws Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long startGenTimestamp = System.currentTimeMillis();
+
+        List<String> generatedGarbageBasePaths = new ArrayList<>();
+
+        int numberOfRuns = options.getNumberOfRuns();
+        int intervalSeconds = options.getGenerateIntervalSeconds();
+        Runnable task = () -> {
+            if (continuousRunIndex < numberOfRuns) {
+                try {
+                    String genBasePath = generateGarbage(options, continuousRunIndex, builder, startGenTimestamp);
+                    generatedGarbageBasePaths.add(genBasePath);
+                } catch (Exception e) {
+                    LOG.error("Error generating garbage in run " + continuousRunIndex, e);
+                }
+                LOG.info("Task executed. Count: " + (continuousRunIndex + 1));
+                continuousRunIndex++;
+            } else {
+                // Shutdown the executor once the task has run numberOfRuns times
+                continuousRunExecutor.shutdown();
+                LOG.info("Task completed " + numberOfRuns + " times. Stopping execution.");
+            }
+        };
+
+        // Schedule the task to run every intervalSeconds
+        continuousRunExecutor.scheduleAtFixedRate(task, 0, intervalSeconds, TimeUnit.SECONDS);
+
+        return generatedGarbageBasePaths;
+    }
+
+    /**
+     * Generate garbage nodes in the repository in order to allow for testing fullGC functionality.
+     *
+     * Returns the path of the generated FULLGC_GEN_BASE_PATH node (under the root).
+     * @param options
+     * @param closer
+     * @param runIndex
+     * @return
+     * @throws IOException
+     * @throws Exception
+     */
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long generationTimestamp = System.currentTimeMillis();
+
+        return generateGarbage(options, runIndex, builder, generationTimestamp);
+    }
+
+    private String generateGarbage(GenerateFullGCOptions options, int runIndex,
+                                   DocumentNodeStoreBuilder<?> builder, long timestamp) throws Exception {
+
+        if (builder == null) {
+            System.err.println("generateFullGC mode only available for DocumentNodeStore");
+            System.exit(1);
+        }
+
+        if (GenerateGarbageHelper.isInvalidGarbageGenerationMode(options.getGarbageType())) {
+            LOG.error("Invalid fullGCMode specified: " + options.getGarbageType() + " in: " + getClass().getName());
+            System.exit(1);
+        }
+
+        String generationBasePath = GEN_BASE_PATH + timestamp + "_" + runIndex;
+        documentNodeStore = builder.build();
+
+        NodeBuilder rootNode = documentNodeStore.getRoot().builder();
+
+        if (GenerateGarbageHelper.isEmptyProps(options.getGarbageType())) {
+            generateGarbageEmptyProps(rootNode, options, generationBasePath);
+        }
+        else if (GenerateGarbageHelper.isGapOrphans(options.getGarbageType())) {
+            generateGarbageGapOrphans(rootNode, options, generationBasePath);
+        }
+
+        return generationBasePath;
+    }
+    private void generateGarbageGapOrphans(NodeBuilder rootNode, GenerateFullGCOptions options, String generationBasePath)
+            throws CommitFailedException {
+
+        // validate gap orphans depth and level gap
+        if (options.getOrphansDepth() < 0 || options.getOrphansDepth() > GAP_ORPHANS_MAX_DEPTH) {
+            LOG.error("Invalid orphansDepth specified: " + options.getOrphansDepth() + " in: " + getClass().getName()
+                    + ". Must be an integer >= 0 and <= " + GAP_ORPHANS_MAX_DEPTH);
+            System.exit(1);
+        }
+        if (options.getOrphansLevelGap() < 0 || options.getOrphansLevelGap() > options.getOrphansDepth()) {
+            LOG.error("Invalid orphansLevelGap specified: " + options.getOrphansLevelGap() + " in: " + getClass().getName()
+                    + ". Must be a positive integer <= orphansDepth");
+            System.exit(1);
+        }
+
+        System.out.println("Generating fullGC gap orphans on the document: " + generationBasePath);
+
+        NodeBuilder garbageRootNode = rootNode.child(GARBAGE_GEN_ROOT_PATH_BASE).child(GARBAGE_GEN_ROOT_NODE_NAME);
+        garbageRootNode.child(generationBasePath).setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+
+        int nodesCountUnderParent = options.getCreateGarbageNodesCount() / options.getGarbageNodesParentCount();
+        for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+            // create parent node
+            garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).setProperty(JcrConstants.JCR_PRIMARYTYPE, "nt:folder", NAME);
+
+            // create child nodes under parent, according to gap orphans depth
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                getGapOrphanLeafGarbageNode(garbageRootNode, generationBasePath, options.getOrphansDepth(), i, j).
+                        setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+            }
+        }
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+
+        // Generate garbage nodes - GAP_ORPHANS - remove parent nodes
+        StringBuilder sbNodePath = new StringBuilder();
+        List<String> deleteNodePaths = new ArrayList<>();
+        for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+
+            // append the parent to the paths to delete
+            sbNodePath.setLength(0);
+            String path = getIdFromPath(
+                    sbNodePath.append("/").append(GARBAGE_GEN_ROOT_PATH).append("/").append(generationBasePath).append("/")
+                            .append(GEN_PARENT_NODE_PREFIX).append(i).toString());
+            deleteNodePaths.add(path);
+
+            // append all the gap orphans nodes between the parent and the level gap to the paths to delete
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                deleteNodePaths.addAll(generateGapOrphanNodePaths(generationBasePath, i, j, options.getOrphansDepth(), options.getOrphansLevelGap(), false));
+            }
+        }
+        // Remove all parent nodes
+        documentNodeStore.getDocumentStore().remove(org.apache.jackrabbit.oak.plugins.document.Collection.NODES, deleteNodePaths);
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+    }
+
+    private NodeBuilder getGapOrphanLeafGarbageNode(NodeBuilder garbageRootNode, String generationBasePath, int depth, int parentIndex, int nodeIndex) {
+        NodeBuilder garbageNode = garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + parentIndex);
+        for (int i = 1; i < depth; i++) {
+            garbageNode = garbageNode.child(GEN_NODE_PREFIX + nodeIndex + GEN_NODE_LEVEL_PREFIX + i);
+        }
+        // the leaf garbage node does not have level in its name
+        garbageNode = garbageNode.child(GEN_NODE_PREFIX + nodeIndex);
+        return garbageNode;
+    }
+
+    public static List<String> generateGapOrphanNodePaths(String generationBasePath, int parentIndex, int nodeIndex, int gapDepth) {
+        return generateGapOrphanNodePaths(generationBasePath, parentIndex, nodeIndex, gapDepth, 0, false);
+    }
+
+    /**
+     * Gets the paths of the nodes between the parent node and the level gap OR between the level gap and the leaf
+     * gap orphan node.
+     *
+     * Example 1: gapDepth = 4, gapLevelGap = 2, fromLevelGapToLeaf = false will return:
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2
+     * and will NOT return the rest of the nodes:
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3/Node_0 (the leaf node)
+     *
+     * Example 2: gapDepth = 5, gapLevelGap = 2, fromLevelGapToLeaf = true will return:
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3/Node_0_Level_4
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3/Node_0_Level_4/Node_0 (the leaf node)
+     * and will NOT return the nodes from the parent to the gap level:
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2
+     *
+     * @param generationBasePath
+     * @param parentIndex
+     * @param nodeIndex
+     * @param gapDepth - the depth in the tree until which to create gap orphan garbage nodes.
+     * @param gapLevelGap - the gap in the tree between the first gap orphan garbage node and its parent.
+     * @param fromLevelGapToLeaf - if true, the paths will be generated from the level gap to the leaf node, otherwise the paths will be generated from the parent node to the level gap.
+     * @return
+     */
+    public static List<String> generateGapOrphanNodePaths(String generationBasePath, int parentIndex, int nodeIndex, int gapDepth, int gapLevelGap, boolean fromLevelGapToLeaf) {
+        List<String> gapOrphanNodePaths = new ArrayList<>();
+
+        StringBuilder sbNodePath = new StringBuilder();
+        sbNodePath.setLength(0);
+
+        // start path from the parent node
+        sbNodePath.append("/").append(GARBAGE_GEN_ROOT_PATH).append("/").append(generationBasePath).append("/")
+                .append(GEN_PARENT_NODE_PREFIX).append(parentIndex);
+
+        for(int i = 1; i <= gapDepth; i ++) {
+            sbNodePath.append("/").append(GEN_NODE_PREFIX).append(nodeIndex);
+            // the leaf node does NOT have level in its name
+            if (i < gapDepth) {
+                sbNodePath.append(GEN_NODE_LEVEL_PREFIX).append(i);
+            }
+
+            // only append to list if the criteria for returning the node path is met
+            if ((!fromLevelGapToLeaf && i < gapLevelGap) || (fromLevelGapToLeaf && i >= gapLevelGap)) {
+                gapOrphanNodePaths.add(getIdFromPath(sbNodePath.toString()));
+            }
+        }
+
+        return gapOrphanNodePaths;
+    }
+
+    private void generateGarbageEmptyProps(NodeBuilder rootNode, GenerateFullGCOptions options, String generationBasePath) throws CommitFailedException {
+
+        System.out.println("Generating fullGC empty props on the document: " + generationBasePath);
+
+        NodeBuilder garbageRootNode = rootNode.child(GARBAGE_GEN_ROOT_PATH_BASE).child(GARBAGE_GEN_ROOT_NODE_NAME);
+        garbageRootNode.child(generationBasePath).setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+
+        int nodesCountUnderParent = options.getCreateGarbageNodesCount() / options.getGarbageNodesParentCount();
+        for(int i = 0; i < options.getGarbageNodesParentCount(); i ++) {
+            garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).setProperty(JcrConstants.JCR_PRIMARYTYPE, "nt:folder", NAME);
+
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                        setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+
+                if (GenerateGarbageHelper.isEmptyProps(options.getGarbageType())) {
+                    garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                            setProperty(EMPTY_PROPERTY_NAME, "bar", STRING);
+                }
+            }
+        }
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+
+
+        //2. Generate garbage nodes - EMPTY_PROPERTIES
+        for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+            for (int j = 0; j < nodesCountUnderParent; j++) {
+                garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                        removeProperty(EMPTY_PROPERTY_NAME);
+            }
+        }
+
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+    }
+
+    /**
+     * Cleans up all generated garbage by removing the node FULLGC_GEN_ROOT_PATH and all of
+     * its children (recursively)
+     * @param options
+     * @param closer
+     * @throws IOException
+     * @throws Exception
+     */
+    private void cleanGarbage(GenerateFullGCOptions options, Closer closer) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+
+        if (builder == null) {
+            System.err.println("generateFullGC mode only available for DocumentNodeStore");
+            System.exit(1);
+        }
+
+        System.out.println("Cleaning up all generated garbage:");
+        documentNodeStore = builder.build();
+
+        NodeBuilder rootBuilder = documentNodeStore.getRoot().builder();
+
+        NodeBuilder generatedGarbageRootBuilder = rootBuilder.child(GARBAGE_GEN_ROOT_PATH_BASE).child(GARBAGE_GEN_ROOT_NODE_NAME);
+
+        String garbageRootNodePath = "2:/"+ GARBAGE_GEN_ROOT_PATH;
+        List<String> childNodePaths = new ArrayList<>();
+        childNodePaths.add(garbageRootNodePath);
+
+        // get all paths of the tree nodes under the garbage root node
+        getTreeNodePaths(generatedGarbageRootBuilder, garbageRootNodePath, childNodePaths, 2);
+
+        documentNodeStore.getDocumentStore().remove(org.apache.jackrabbit.oak.plugins.document.Collection.NODES, childNodePaths);
+        documentNodeStore.merge(rootBuilder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();

Review Comment:
   Changed, thank you for the suggestion.



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateGarbageCommand.java:
##########
@@ -0,0 +1,553 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.commons.concurrent.ExecutorCloser;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.GenerateGarbageHelper;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateGarbageCommand implements Command, Closeable {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateGarbageCommand.class);
+
+    private final ScheduledExecutorService continuousRunExecutor = Executors.newScheduledThreadPool(1);
+
+    private static final String USAGE = "createGarbage {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * FullGC garbage should be generated under tmp node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH_BASE = "tmp";
+
+    /**
+     * The root node name for fullGC garbage generation, one level under tmp.
+     */
+    public static String GARBAGE_GEN_ROOT_NODE_NAME = "fullGC_GarbageRoot";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH = GARBAGE_GEN_ROOT_PATH_BASE + "/" + GARBAGE_GEN_ROOT_NODE_NAME;
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String GEN_BASE_PATH = "GenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String GEN_PARENT_NODE_PREFIX = "Parent_";
+    public static String GEN_NODE_PREFIX = "Node_";
+    public static String GEN_NODE_LEVEL_PREFIX = "_Level_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    /**
+     * The maximum depth in the tree at which to generate gap orphans garbage nodes.
+     * Used for validation of the orphansDepth / orphansLevelGap options.
+     */
+    private final int GAP_ORPHANS_MAX_DEPTH = 15;
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node FULLGC_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> orphansDepth;
+        final OptionSpec<Integer> orphansLevelGap;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public GenerateFullGCOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            orphansDepth = parser
+                    .accepts("orphansDepth", "the depth in the tree at which to create garbage nodes. Only applies to ORPHANS fullGC modes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            orphansLevelGap = parser
+                    .accepts("orphansLevelGap", "the gap in the tree between the first leaf gap orphan garbage node and its' parent. Only applies to ORPHANS fullGC modes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public GenerateFullGCOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getOrphansDepth() { return orphansDepth.value(options);}
+
+        public int getOrphansLevelGap() { return orphansLevelGap.value(options); }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {
+
+        try (Closer closer = Closer.create()) {
+            // Register the ExecutorService with Closer
+            closer.register(() -> {
+                // Properly shutdown the executor service
+                continuousRunExecutor.shutdown();
+                try {
+                    if (!continuousRunExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
+                        continuousRunExecutor.shutdownNow();
+                    }
+                } catch (InterruptedException e) {
+                    throw new RuntimeException(e);
+                }
+            });
+            execute(closer, args);
+        } catch (Throwable e) {
+            LOG.error("Command failed", e);
+            throw e;
+        }
+    }
+
+    /**
+     * Method with passed closer is necessary in order to allow for unit tests to check the output of the command.
+     * It is the responsibility of the caller to close the closer.
+     *
+     * Returns the list of generated garbage base paths (under the garbage root node).
+     * @param closer
+     * @param args
+     * @throws Exception
+     */
+    public List<String> execute(Closer closer, String... args) throws Exception {
+        continuousRunIndex = 0;
+
+        List<String> generateBasePaths = new ArrayList<>();
+
+        GenerateFullGCOptions options = new GenerateFullGCOptions(USAGE).parse(args);
+        String subCmd = options.getSubCmd();
+
+        if (GenerateFullGCOptions.CMD_GENERATE.equals(subCmd)) {
+
+            if (options.getCreateGarbageNodesCount() <= 0) {
+                LOG.error("Invalid garbageNodesCount specified: " + options.getCreateGarbageNodesCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+            if (options.getGarbageNodesParentCount() <= 0) {
+                LOG.error("Invalid garbageNodesParentCount specified: " + options.getGarbageNodesParentCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+
+            if (options.getNumberOfRuns() > 1 && options.getGenerateIntervalSeconds() > 0) {
+                generateBasePaths.addAll(generateGarbageContinuously(options, closer));
+            } else {
+                generateBasePaths.add(generateGarbage(options, closer, 0));
+            }
+        } else if (GenerateFullGCOptions.CMD_CLEAN.equals(subCmd)) {
+            cleanGarbage(options, closer);
+        } else {
+            System.err.println("Unknown revisions command: " + subCmd);
+        }
+
+        return generateBasePaths;
+    }
+
+    @Override
+    public void close() throws IOException {
+        new ExecutorCloser(this.continuousRunExecutor).close();
+    }
+
+    private List<String> generateGarbageContinuously(GenerateFullGCOptions options, Closer closer) throws Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long startGenTimestamp = System.currentTimeMillis();
+
+        List<String> generatedGarbageBasePaths = new ArrayList<>();
+
+        int numberOfRuns = options.getNumberOfRuns();
+        int intervalSeconds = options.getGenerateIntervalSeconds();
+        Runnable task = () -> {
+            if (continuousRunIndex < numberOfRuns) {
+                try {
+                    String genBasePath = generateGarbage(options, continuousRunIndex, builder, startGenTimestamp);
+                    generatedGarbageBasePaths.add(genBasePath);
+                } catch (Exception e) {
+                    LOG.error("Error generating garbage in run " + continuousRunIndex, e);
+                }
+                LOG.info("Task executed. Count: " + (continuousRunIndex + 1));
+                continuousRunIndex++;
+            } else {
+                // Shutdown the executor once the task has run numberOfRuns times
+                continuousRunExecutor.shutdown();
+                LOG.info("Task completed " + numberOfRuns + " times. Stopping execution.");
+            }
+        };
+
+        // Schedule the task to run every intervalSeconds
+        continuousRunExecutor.scheduleAtFixedRate(task, 0, intervalSeconds, TimeUnit.SECONDS);
+
+        return generatedGarbageBasePaths;
+    }
+
+    /**
+     * Generate garbage nodes in the repository in order to allow for testing fullGC functionality.
+     *
+     * Returns the path of the generated FULLGC_GEN_BASE_PATH node (under the root).
+     * @param options
+     * @param closer
+     * @param runIndex
+     * @return
+     * @throws IOException
+     * @throws Exception
+     */
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long generationTimestamp = System.currentTimeMillis();
+
+        return generateGarbage(options, runIndex, builder, generationTimestamp);
+    }
+
+    private String generateGarbage(GenerateFullGCOptions options, int runIndex,
+                                   DocumentNodeStoreBuilder<?> builder, long timestamp) throws Exception {
+
+        if (builder == null) {
+            System.err.println("generateFullGC mode only available for DocumentNodeStore");
+            System.exit(1);
+        }
+
+        if (GenerateGarbageHelper.isInvalidGarbageGenerationMode(options.getGarbageType())) {
+            LOG.error("Invalid fullGCMode specified: " + options.getGarbageType() + " in: " + getClass().getName());
+            System.exit(1);
+        }
+
+        String generationBasePath = GEN_BASE_PATH + timestamp + "_" + runIndex;
+        documentNodeStore = builder.build();
+
+        NodeBuilder rootNode = documentNodeStore.getRoot().builder();
+
+        if (GenerateGarbageHelper.isEmptyProps(options.getGarbageType())) {
+            generateGarbageEmptyProps(rootNode, options, generationBasePath);
+        }
+        else if (GenerateGarbageHelper.isGapOrphans(options.getGarbageType())) {
+            generateGarbageGapOrphans(rootNode, options, generationBasePath);
+        }
+
+        return generationBasePath;
+    }
+    private void generateGarbageGapOrphans(NodeBuilder rootNode, GenerateFullGCOptions options, String generationBasePath)
+            throws CommitFailedException {
+
+        // validate gap orphans depth and level gap
+        if (options.getOrphansDepth() < 0 || options.getOrphansDepth() > GAP_ORPHANS_MAX_DEPTH) {
+            LOG.error("Invalid orphansDepth specified: " + options.getOrphansDepth() + " in: " + getClass().getName()
+                    + ". Must be an integer >= 0 and <= " + GAP_ORPHANS_MAX_DEPTH);
+            System.exit(1);
+        }
+        if (options.getOrphansLevelGap() < 0 || options.getOrphansLevelGap() > options.getOrphansDepth()) {
+            LOG.error("Invalid orphansLevelGap specified: " + options.getOrphansLevelGap() + " in: " + getClass().getName()
+                    + ". Must be a positive integer <= orphansDepth");
+            System.exit(1);
+        }
+
+        System.out.println("Generating fullGC gap orphans on the document: " + generationBasePath);
+
+        NodeBuilder garbageRootNode = rootNode.child(GARBAGE_GEN_ROOT_PATH_BASE).child(GARBAGE_GEN_ROOT_NODE_NAME);
+        garbageRootNode.child(generationBasePath).setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+
+        int nodesCountUnderParent = options.getCreateGarbageNodesCount() / options.getGarbageNodesParentCount();
+        for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+            // create parent node
+            garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).setProperty(JcrConstants.JCR_PRIMARYTYPE, "nt:folder", NAME);
+
+            // create child nodes under parent, according to gap orphans depth
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                getGapOrphanLeafGarbageNode(garbageRootNode, generationBasePath, options.getOrphansDepth(), i, j).
+                        setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+            }
+        }
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+
+        // Generate garbage nodes - GAP_ORPHANS - remove parent nodes
+        StringBuilder sbNodePath = new StringBuilder();
+        List<String> deleteNodePaths = new ArrayList<>();
+        for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+
+            // append the parent to the paths to delete
+            sbNodePath.setLength(0);
+            String path = getIdFromPath(
+                    sbNodePath.append("/").append(GARBAGE_GEN_ROOT_PATH).append("/").append(generationBasePath).append("/")
+                            .append(GEN_PARENT_NODE_PREFIX).append(i).toString());
+            deleteNodePaths.add(path);
+
+            // append all the gap orphans nodes between the parent and the level gap to the paths to delete
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                deleteNodePaths.addAll(generateGapOrphanNodePaths(generationBasePath, i, j, options.getOrphansDepth(), options.getOrphansLevelGap(), false));
+            }
+        }
+        // Remove all parent nodes
+        documentNodeStore.getDocumentStore().remove(org.apache.jackrabbit.oak.plugins.document.Collection.NODES, deleteNodePaths);
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+    }
+
+    private NodeBuilder getGapOrphanLeafGarbageNode(NodeBuilder garbageRootNode, String generationBasePath, int depth, int parentIndex, int nodeIndex) {
+        NodeBuilder garbageNode = garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + parentIndex);
+        for (int i = 1; i < depth; i++) {
+            garbageNode = garbageNode.child(GEN_NODE_PREFIX + nodeIndex + GEN_NODE_LEVEL_PREFIX + i);
+        }
+        // the leaf garbage node does not have level in its name
+        garbageNode = garbageNode.child(GEN_NODE_PREFIX + nodeIndex);
+        return garbageNode;
+    }
+
+    public static List<String> generateGapOrphanNodePaths(String generationBasePath, int parentIndex, int nodeIndex, int gapDepth) {
+        return generateGapOrphanNodePaths(generationBasePath, parentIndex, nodeIndex, gapDepth, 0, false);
+    }
+
+    /**
+     * Gets the paths of the nodes between the parent node and the level gap OR between the level gap and the leaf
+     * gap orphan node.
+     *
+     * Example 1: gapDepth = 4, gapLevelGap = 2, fromLevelGapToLeaf = false will return:
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2
+     * and will NOT return the rest of the nodes:
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3/Node_0 (the leaf node)
+     *
+     * Example 2: gapDepth = 5, gapLevelGap = 2, fromLevelGapToLeaf = true will return:
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3/Node_0_Level_4
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3/Node_0_Level_4/Node_0 (the leaf node)
+     * and will NOT return the nodes from the parent to the gap level:
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2
+     *
+     * @param generationBasePath
+     * @param parentIndex
+     * @param nodeIndex
+     * @param gapDepth - the depth in the tree until which to create gap orphan garbage nodes.
+     * @param gapLevelGap - the gap in the tree between the first gap orphan garbage node and its parent.
+     * @param fromLevelGapToLeaf - if true, the paths will be generated from the level gap to the leaf node, otherwise the paths will be generated from the parent node to the level gap.
+     * @return
+     */
+    public static List<String> generateGapOrphanNodePaths(String generationBasePath, int parentIndex, int nodeIndex, int gapDepth, int gapLevelGap, boolean fromLevelGapToLeaf) {
+        List<String> gapOrphanNodePaths = new ArrayList<>();
+
+        StringBuilder sbNodePath = new StringBuilder();
+        sbNodePath.setLength(0);
+
+        // start path from the parent node
+        sbNodePath.append("/").append(GARBAGE_GEN_ROOT_PATH).append("/").append(generationBasePath).append("/")
+                .append(GEN_PARENT_NODE_PREFIX).append(parentIndex);
+
+        for(int i = 1; i <= gapDepth; i ++) {
+            sbNodePath.append("/").append(GEN_NODE_PREFIX).append(nodeIndex);
+            // the leaf node does NOT have level in its name
+            if (i < gapDepth) {
+                sbNodePath.append(GEN_NODE_LEVEL_PREFIX).append(i);
+            }
+
+            // only append to list if the criteria for returning the node path is met
+            if ((!fromLevelGapToLeaf && i < gapLevelGap) || (fromLevelGapToLeaf && i >= gapLevelGap)) {
+                gapOrphanNodePaths.add(getIdFromPath(sbNodePath.toString()));
+            }
+        }
+
+        return gapOrphanNodePaths;
+    }
+
+    private void generateGarbageEmptyProps(NodeBuilder rootNode, GenerateFullGCOptions options, String generationBasePath) throws CommitFailedException {
+
+        System.out.println("Generating fullGC empty props on the document: " + generationBasePath);
+
+        NodeBuilder garbageRootNode = rootNode.child(GARBAGE_GEN_ROOT_PATH_BASE).child(GARBAGE_GEN_ROOT_NODE_NAME);
+        garbageRootNode.child(generationBasePath).setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+
+        int nodesCountUnderParent = options.getCreateGarbageNodesCount() / options.getGarbageNodesParentCount();
+        for(int i = 0; i < options.getGarbageNodesParentCount(); i ++) {
+            garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).setProperty(JcrConstants.JCR_PRIMARYTYPE, "nt:folder", NAME);
+
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                        setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+
+                if (GenerateGarbageHelper.isEmptyProps(options.getGarbageType())) {
+                    garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                            setProperty(EMPTY_PROPERTY_NAME, "bar", STRING);
+                }
+            }
+        }
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+
+
+        //2. Generate garbage nodes - EMPTY_PROPERTIES
+        for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+            for (int j = 0; j < nodesCountUnderParent; j++) {
+                garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                        removeProperty(EMPTY_PROPERTY_NAME);
+            }
+        }
+
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();

Review Comment:
   Changed, thank you for the suggestion.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "shodaaan (via GitHub)" <gi...@apache.org>.
shodaaan commented on PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#issuecomment-2444268556

   > > Regarding the explicit check, I don't think this is necessary
   > 
   > Fair enough.
   > 
   > Two unrelated additional comments:
   > 
   > * the DocumentNodeStore is not properly disposed. You can see this by running the [oak-run](https://issues.apache.org/jira/browse/OAK-run) command consecutively : the second start will have to wait 2min for the lease timeout of the first non-gracefully shut down run. We should ensure a graceful disposal.
   > * what might be useful is to add the base path where the garbage is generated to - such that the user immediately knows (and doesn't eg have to consult the source code)
   
   I added documentNodeStore.dispose() calls to CreateGarbageCommand. Thank you for the suggestion
   
   The root path where garbage is generated (/tmp/oak-run-generated-test-garbage) is specified in the USAGE string of CreateGarbageCommand, so users will know what the path is where garbage is generated.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "stefan-egli (via GitHub)" <gi...@apache.org>.
stefan-egli commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1806355826


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateFullGCCommand.java:
##########
@@ -0,0 +1,416 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.plugins.document.Collection;
+import org.apache.jackrabbit.oak.plugins.document.Document;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.DocumentStoreException;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateFullGCCommand implements Command {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateFullGCCommand.class);
+
+    private static final String USAGE = "generateFullGC {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String FULLGC_GEN_ROOT_PATH = "fullGCGenRoot";
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String FULLGC_GEN_BASE_PATH = "fullGCGenTest_";

Review Comment:
   once you are under `FULLGC_GEN_ROOT_PATH` and its name indicates that it is for testing and is garbage, do we still need all path elements to contain `fullGc`?



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateFullGCCommand.java:
##########
@@ -0,0 +1,416 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.plugins.document.Collection;
+import org.apache.jackrabbit.oak.plugins.document.Document;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.DocumentStoreException;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateFullGCCommand implements Command {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateFullGCCommand.class);
+
+    private static final String USAGE = "generateFullGC {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String FULLGC_GEN_ROOT_PATH = "fullGCGenRoot";

Review Comment:
   Would recommend not to add content to root and use something like "/tmp" instead, so eg "/tmp/fullGcTestGarbage" or something like that.



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateFullGCCommand.java:
##########
@@ -0,0 +1,416 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.plugins.document.Collection;
+import org.apache.jackrabbit.oak.plugins.document.Document;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.DocumentStoreException;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateFullGCCommand implements Command {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateFullGCCommand.class);
+
+    private static final String USAGE = "generateFullGC {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String FULLGC_GEN_ROOT_PATH = "fullGCGenRoot";
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String FULLGC_GEN_BASE_PATH = "fullGCGenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String FULLGC_GEN_PARENT_NODE_PREFIX = "fullGCParent_";
+    public static String FULLGC_GEN_NODE_PREFIX = "fullGCNode_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node FULLGC_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public GenerateFullGCOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public GenerateFullGCOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {
+        Closer closer = Closer.create();
+        try {
+            execute(closer, args);
+        } catch (Throwable e) {
+            LOG.error("Command failed", e);
+            throw closer.rethrow(e);
+        } finally {
+            closer.close();
+        }
+    }
+
+    /**
+     * Method with passed closer is necessary in order to allow for unit tests to check the output of the command.
+     * It is the responsibility of the caller to close the closer.
+     *
+     * Returns the list of generated garbage base paths (under the garbage root node).
+     * @param closer
+     * @param args
+     * @throws Exception
+     */
+    public List<String> execute(Closer closer, String... args) throws Exception {
+        continuousRunIndex = 0;
+
+        List<String> generateBasePaths = new ArrayList<>();
+
+        GenerateFullGCOptions options = new GenerateFullGCOptions(USAGE).parse(args);
+        String subCmd = options.getSubCmd();
+
+        if (GenerateFullGCOptions.CMD_GENERATE.equals(subCmd)) {
+            if (options.getNumberOfRuns() > 1 && options.getGenerateIntervalSeconds() > 0) {
+                generateBasePaths.addAll(generateGarbageContinuously(options, closer));
+            } else {
+                generateBasePaths.add(generateGarbage(options, closer, 0));
+            }
+        } else if (GenerateFullGCOptions.CMD_CLEAN.equals(subCmd)) {
+            cleanGarbage(options, closer);
+        } else {
+            System.err.println("unknown revisions command: " + subCmd);
+        }
+
+        return generateBasePaths;
+    }
+
+    private List<String> generateGarbageContinuously(GenerateFullGCOptions options, Closer closer) throws IOException, Exception {
+        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long startGenTimestamp = System.currentTimeMillis();
+
+        List<String> generatedGarbageBasePaths = new ArrayList<>();
+
+        int numberOfRuns = options.getNumberOfRuns();
+        int intervalSeconds = options.getGenerateIntervalSeconds();
+        Runnable task = () -> {
+            if (continuousRunIndex < numberOfRuns) {
+                try {
+                    String genBasePath = generateGarbage(options, closer, continuousRunIndex, builder, startGenTimestamp);
+                    generatedGarbageBasePaths.add(genBasePath);
+                } catch (Exception e) {
+                    LOG.error("Error generating garbage in run " + continuousRunIndex, e);
+                }
+                LOG.info("Task executed. Count: " + (continuousRunIndex + 1));
+                continuousRunIndex++;
+            } else {
+                // Shutdown the executor once the task has run numberOfRuns times
+                executor.shutdown();
+                LOG.info("Task completed " + numberOfRuns + " times. Stopping execution.");
+            }
+        };
+
+        // Schedule the task to run every intervalSeconds
+        executor.scheduleAtFixedRate(task, 0, intervalSeconds, TimeUnit.SECONDS);
+
+        return generatedGarbageBasePaths;
+    }
+
+    /**
+     * Generate garbage nodes in the repository in order to allow for testing fullGC functionality.
+     *
+     * Returns the path of the generated FULLGC_GEN_BASE_PATH node (under the root).
+     * @param options
+     * @param closer
+     * @param runIndex
+     * @return
+     * @throws IOException
+     * @throws Exception
+     */
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long generationTimestamp = System.currentTimeMillis();
+
+        return generateGarbage(options, closer, runIndex, builder, generationTimestamp);
+    }
+
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex,
+                                  DocumentNodeStoreBuilder<?> builder, long timestamp) throws IOException, Exception {
+
+        if (builder == null) {
+            System.err.println("generateFullGC mode only available for DocumentNodeStore");
+            System.exit(1);
+        }
+
+        String generationBasePath = FULLGC_GEN_BASE_PATH + timestamp + "_" + runIndex;
+        System.out.println("Generating fullGC on the document: " + generationBasePath);
+        documentNodeStore = builder.build();
+
+        VersionGarbageCollector.FullGCMode fullGCMode = getFullGCMode(options);
+        if (fullGCMode == VersionGarbageCollector.FullGCMode.NONE) {
+            LOG.error("Invalid garbageType specified. Must be one of the following: 1 - EMPTYPROPS, 2 - GAP_ORPHANS, 3 - GAP_ORPHANS_EMPTYPROPS");
+            System.exit(1);
+        }
+
+        //1. Create nodes with properties
+        NodeBuilder rootNode = documentNodeStore.getRoot().builder();
+        NodeBuilder garbageRootNode = rootNode.child(FULLGC_GEN_ROOT_PATH);
+        garbageRootNode.child(generationBasePath).setProperty("jcr:primaryType", "nt:file", NAME);
+
+        int nodesCountUnderParent = options.getCreateGarbageNodesCount() / options.getGarbageNodesParentCount();
+        for(int i = 0; i < options.getGarbageNodesParentCount(); i ++) {
+            garbageRootNode.child(generationBasePath).child(FULLGC_GEN_PARENT_NODE_PREFIX + i).setProperty("jcr:primaryType", "nt:folder", NAME);
+
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                garbageRootNode.child(generationBasePath).child(FULLGC_GEN_PARENT_NODE_PREFIX + i).child(FULLGC_GEN_NODE_PREFIX + j).
+                        setProperty("jcr:primaryType", "nt:file", NAME);
+
+                if (fullGCMode == VersionGarbageCollector.FullGCMode.EMPTYPROPS || fullGCMode == VersionGarbageCollector.FullGCMode.GAP_ORPHANS_EMPTYPROPS) {
+                    garbageRootNode.child(generationBasePath).child(FULLGC_GEN_PARENT_NODE_PREFIX + i).child(FULLGC_GEN_NODE_PREFIX + j).
+                            setProperty(EMPTY_PROPERTY_NAME, "bar", STRING);
+                }
+            }
+        }
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+
+
+        //2. Generate garbage nodes - EMPTY_PROPERTIES
+        if (fullGCMode == VersionGarbageCollector.FullGCMode.EMPTYPROPS) {
+            for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+                for (int j = 0; j < nodesCountUnderParent; j++) {
+                    garbageRootNode.child(generationBasePath).child(FULLGC_GEN_PARENT_NODE_PREFIX + i).child(FULLGC_GEN_NODE_PREFIX + j).
+                            removeProperty(EMPTY_PROPERTY_NAME);
+                }
+            }
+        }
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+
+        //3.1. Generate garbage nodes - GAP_ORPHANS - remove parent nodes
+        if (fullGCMode == VersionGarbageCollector.FullGCMode.GAP_ORPHANS) {
+            StringBuilder sbNodePath = new StringBuilder();
+            List<String> deleteNodePaths = new ArrayList<>();
+            for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+
+                sbNodePath.setLength(0);
+                sbNodePath.append("3:/").append(FULLGC_GEN_ROOT_PATH).append("/").append(generationBasePath).append("/").
+                        append(FULLGC_GEN_PARENT_NODE_PREFIX).append(i);
+                deleteNodePaths.add(sbNodePath.toString());
+            }
+            // Remove all parent nodes
+            documentNodeStore.getDocumentStore().remove(org.apache.jackrabbit.oak.plugins.document.Collection.NODES, deleteNodePaths);
+            documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+            documentNodeStore.runBackgroundOperations();
+        }
+
+        return generationBasePath;
+    }
+
+    /**
+     * Cleans up all generated garbage by removing the node FULLGC_GEN_ROOT_PATH and all of
+     * its children (recursively)
+     * @param options
+     * @param closer
+     * @throws IOException
+     * @throws Exception
+     */
+    private void cleanGarbage(GenerateFullGCOptions options, Closer closer) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+
+        if (builder == null) {
+            System.err.println("generateFullGC mode only available for DocumentNodeStore");
+            System.exit(1);
+        }
+
+        System.out.println("Cleaning up all generated garbage:");
+        documentNodeStore = builder.build();
+
+        NodeBuilder rootBuilder = documentNodeStore.getRoot().builder();
+
+        NodeBuilder generatedGarbageRootBuilder = rootBuilder.child(FULLGC_GEN_ROOT_PATH);
+
+        String garbageRootNodePath = "1:/"+FULLGC_GEN_ROOT_PATH;
+        List<String> childNodePaths = new ArrayList<>();
+        childNodePaths.add(garbageRootNodePath);
+
+        // get all paths of the tree nodes under the garbage root node
+        getTreeNodePaths(generatedGarbageRootBuilder, garbageRootNodePath, childNodePaths, 1);
+
+        documentNodeStore.getDocumentStore().remove(org.apache.jackrabbit.oak.plugins.document.Collection.NODES, childNodePaths);
+        documentNodeStore.merge(rootBuilder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+    }
+
+    /**
+     * Recursively get all paths of the tree nodes under the given root node.
+     * @param rootNode
+     * @param basePath
+     * @param treeNodePaths
+     * @param nodeLevel
+     */
+    private void getTreeNodePaths(NodeBuilder rootNode, String basePath,
+                                  List<String> treeNodePaths, int nodeLevel) {
+        String childBasePath = basePath.replaceFirst(nodeLevel + ":/", (nodeLevel + 1) + ":/");
+        for (String childNodeName : rootNode.getChildNodeNames()) {
+            String childPath = childBasePath + "/" + childNodeName;
+            treeNodePaths.add(childPath);
+            getTreeNodePaths(rootNode.child(childNodeName), childPath, treeNodePaths, nodeLevel + 1);
+        }
+    }
+
+    /**
+     * Used in unit tests for retrieving a document by path from the document store using the documentNodeStore
+     * of this class.
+     * @param collection
+     * @param key
+     * @param maxCacheAge
+     * @return
+     * @param <T>
+     * @throws DocumentStoreException
+     */
+    @Nullable
+    public <T extends Document> T getDocument(Collection<T> collection, String key, int maxCacheAge)

Review Comment:
   as this is only used by test code I'd move it over there (and if needed create an accessor to `documentNodeStore` for example)



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/AvailableModes.java:
##########
@@ -75,5 +75,6 @@ public final class AvailableModes {
             .put("segment-copy", new SegmentCopyCommand())
             .put("server", new ServerCommand())
             .put("purge-index-versions", new LucenePurgeOldIndexVersionCommand())
+            .put("generateFullGC", new GenerateFullGCCommand())

Review Comment:
   +1 for renaming the command



##########
oak-run/src/test/java/org/apache/jackrabbit/oak/plugins/document/GenerateFullGCCommandTest.java:
##########
@@ -0,0 +1,293 @@
+/*
+ * 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 org.apache.jackrabbit.guava.common.collect.ImmutableList;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.plugins.document.util.MongoConnection;
+import org.apache.jackrabbit.oak.run.GenerateFullGCCommand;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.apache.jackrabbit.oak.plugins.document.CommandTestUtils.captureSystemOut;
+import static org.apache.jackrabbit.oak.run.GenerateFullGCCommand.EMPTY_PROPERTY_NAME;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+public class GenerateFullGCCommandTest {
+
+    final String OPTION_GARBAGE_NODES_COUNT = "--garbageNodesCount";
+    final String OPTION_GARBAGE_NODES_PARENT_COUNT = "--garbageNodesParentCount";
+    final String OPTION_GARBAGE_TYPE = "--garbageType";
+    final String OPTION_NUMBER_OF_RUNS = "--numberOfRuns";
+    final String OPTION_GENERATE_INTERVAL_SECONDS = "--generateIntervalSeconds";
+
+    @Rule
+    public MongoConnectionFactory connectionFactory = new MongoConnectionFactory();
+
+    @Rule
+    public DocumentMKBuilderProvider builderProvider = new DocumentMKBuilderProvider();
+
+    private DocumentNodeStore ns;
+
+    private Closer closer;
+
+    @BeforeClass
+    public static void assumeMongoDB() {
+        assumeTrue(MongoUtils.isAvailable());
+    }
+
+    @Before
+    public void before() {
+        ns = createDocumentNodeStore();
+    }
+
+    @After
+    public void after() {
+        try {
+            closer.close();
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+        ns.dispose();
+    }
+
+    private DocumentNodeStore createDocumentNodeStore() {
+        MongoConnection c = connectionFactory.getConnection();
+        assertNotNull(c);
+        MongoUtils.dropCollections(c.getDatabase());
+        return builderProvider.newBuilder().setFullGCEnabled(false).setLeaseCheckMode(LeaseCheckMode.DISABLED).setAsyncDelay(0)
+                .setMongoDB(c.getMongoClient(), c.getDBName()).getNodeStore();
+    }
+
+    private List<String> getChildNodeNames(NodeState node) {
+        List<String> childNodeNames = new ArrayList<>();
+        for (String childNodeName : node.getChildNodeNames()) {
+            childNodeNames.add(childNodeName);
+        }
+        return childNodeNames;
+    }
+
+    @Ignore
+    @Test
+    public void generateGarbageEmptyPropsUnderOneParentOnCustomMongoDB() {
+        ns.dispose();
+
+        // this should be a valid mongoDB connection string for the MongoDB on which to generate the garbage
+        String mongoDBConnString = "";
+        GenerateFullGCCmd cmd = new GenerateFullGCCmd( mongoDBConnString, OPTION_GARBAGE_NODES_COUNT, "10", OPTION_GARBAGE_NODES_PARENT_COUNT, "1",
+                OPTION_GARBAGE_TYPE, "1");
+        String output = captureSystemOut(cmd);
+
+        //sleep thread for 10 seconds to allow the garbage generation to complete
+        try {
+            Thread.sleep(10000);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+
+        DocumentNodeStore nodeStore = cmd.getCommand().getDocumentNodeStore();
+        NodeState garbageRootNodeState = nodeStore.getRoot().getChildNode(GenerateFullGCCommand.FULLGC_GEN_ROOT_PATH);
+        List<String> garbageRootNodeChildNames = getChildNodeNames(garbageRootNodeState);
+
+        assertEquals(garbageRootNodeChildNames.size(), 1);
+        List<String> propParentsGarbageNodeNames = getChildNodeNames(garbageRootNodeState.getChildNode(garbageRootNodeChildNames.get(0)));
+        assertEquals(propParentsGarbageNodeNames.size(), 1);
+
+        List<String> propGarbageNodeNames = getChildNodeNames(garbageRootNodeState.getChildNode(garbageRootNodeChildNames.get(0)).getChildNode(propParentsGarbageNodeNames.get(0)));
+        assertEquals(propGarbageNodeNames.size(), 10);
+    }
+
+    @Test
+    public void cleanupAllGarbage() {
+
+        // generate garbage first
+        GenerateFullGCCmd cmdGenerateGarbage = new GenerateFullGCCmd(OPTION_GARBAGE_NODES_COUNT, "10", OPTION_GARBAGE_NODES_PARENT_COUNT, "1",
+                OPTION_GARBAGE_TYPE, "2");
+        cmdGenerateGarbage.run();
+        Closer cmd1Closer = cmdGenerateGarbage.getCloser();
+
+        // cleanup garbage
+        String mongoDBConnString = "mongodb://cm-p107053-e251100-username:3fUEP9UHPD9yP4H@cmp107053e251100r355e01-shard-00-00.wxmqq.mongodb.net:27017,cmp107053e251100r355e01-shard-00-01.wxmqq.mongodb.net:27017,cmp107053e251100r355e01-shard-00-02.wxmqq.mongodb.net:27017/cm-p107053-e251100-database?ssl=true&authSource=admin&replicaSet=atlas-4x602o-shard-0&retryWrites=true&readPreference=primaryPreferred&readConcernLevel=majority&w=majority";
+        GenerateFullGCCmd cmdClean = new GenerateFullGCCmd(mongoDBConnString, "clean");
+        cmdClean.run();
+        closer = cmdClean.getCloser();
+
+        NodeDocument garbageRoot = cmdClean.getCommand().getDocument(Collection.NODES, "1:/" + GenerateFullGCCommand.FULLGC_GEN_ROOT_PATH, 0);
+        // garbage root node should be either deleted or empty (no children)
+        assertTrue(garbageRoot == null || !garbageRoot.hasChildren());
+
+        try {
+            cmd1Closer.close();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Test
+    public void generateGarbageEmptyPropsUnderOneParent() {
+        ns.dispose();
+
+        GenerateFullGCCmd cmd = new GenerateFullGCCmd(OPTION_GARBAGE_NODES_COUNT, "10", OPTION_GARBAGE_NODES_PARENT_COUNT, "1",
+                OPTION_GARBAGE_TYPE, "1");
+        String output = captureSystemOut(cmd);
+        closer = cmd.getCloser();
+
+        List<String> generatedBasePaths = cmd.getGeneratedBasePaths();
+
+        DocumentNodeStore nodeStore = cmd.getCommand().getDocumentNodeStore();
+        NodeState garbageRootNodeState = nodeStore.getRoot().getChildNode(GenerateFullGCCommand.FULLGC_GEN_ROOT_PATH);
+        List<String> garbageRootNodeChildNames = getChildNodeNames(garbageRootNodeState);
+
+        assertEquals(garbageRootNodeChildNames.size(), 1);
+        for (String generateBasePath : generatedBasePaths) {
+            assertTrue(garbageRootNodeChildNames.contains(generateBasePath));
+        }
+
+        List<String> propParentsGarbageNodeNames = getChildNodeNames(garbageRootNodeState.getChildNode(garbageRootNodeChildNames.get(0)));
+        assertEquals(propParentsGarbageNodeNames.size(), 1);
+
+        NodeState emptyPropsParentNode = garbageRootNodeState.getChildNode(garbageRootNodeChildNames.get(0)).getChildNode(propParentsGarbageNodeNames.get(0));
+        List<String> propGarbageNodeNames = getChildNodeNames(emptyPropsParentNode);
+        assertEquals(propGarbageNodeNames.size(), 10);
+
+        for (String propGarbageNodeName : propGarbageNodeNames) {
+            NodeState propGarbageNode = emptyPropsParentNode.getChildNode(propGarbageNodeName);
+            assertNull(propGarbageNode.getProperty(EMPTY_PROPERTY_NAME));
+        }
+    }
+
+    @Test
+    public void generateGarbageGapOrphansUnderOneParent() {
+        ns.dispose();
+
+        int garbageNodesCount = 10;
+        int garbageNodesParentCount = 1;
+
+        testGenerateGapOrphans(garbageNodesCount, garbageNodesParentCount);
+    }
+
+    @Test
+    public void generateGarbageGapOrphansUnderMultipleParents() {
+        ns.dispose();
+
+        int garbageNodesCount = 20;
+        int garbageNodesParentCount = 4;
+
+        testGenerateGapOrphans(garbageNodesCount, garbageNodesParentCount);
+    }
+
+    /**
+     * Calls the GenerateFullGCCommand to generate gap orphans and verifies the generated gap orphans:
+     * - checks that parents of gap orphans are missing by retrieving the nodes from the document store
+     * - checks that the expected number gap orphans are generated by retrieving the nodes from the document store
+     * @param garbageNodesCount
+     * @param garbageNodesParentCount
+     */
+    private void testGenerateGapOrphans(int garbageNodesCount, int garbageNodesParentCount) {
+        GenerateFullGCCmd cmd = new GenerateFullGCCmd(OPTION_GARBAGE_NODES_COUNT, String.valueOf(garbageNodesCount),
+                OPTION_GARBAGE_NODES_PARENT_COUNT, String.valueOf(garbageNodesParentCount),
+                OPTION_GARBAGE_TYPE, "2");
+        String output = captureSystemOut(cmd);
+        closer = cmd.getCloser();
+
+        int nodesPerParent = garbageNodesCount / garbageNodesParentCount;
+
+        List<String> generatedBasePaths = cmd.getGeneratedBasePaths();
+
+        DocumentNodeStore nodeStore = cmd.getCommand().getDocumentNodeStore();
+        NodeState garbageRootNodeState = nodeStore.getRoot().getChildNode(GenerateFullGCCommand.FULLGC_GEN_ROOT_PATH);
+        List<String> garbageRootNodeChildNames = getChildNodeNames(garbageRootNodeState);
+
+        assertEquals(garbageRootNodeChildNames.size(), 1);
+        for (String generateBasePath : generatedBasePaths) {
+            assertTrue(garbageRootNodeChildNames.contains(generateBasePath));
+        }
+
+        String generateBasePath = generatedBasePaths.get(0);
+        for (int i = 0; i < garbageNodesParentCount; i ++) {
+            NodeDocument missingDocument = cmd.getCommand().getDocument(Collection.NODES, "3:/"

Review Comment:
   would suggest to use `Utils.getIdFromPath` etc rather than hard coding the depth - eg changing the base path to be `/tmp/xy` would now require this depth to be fixed
   (applies to a few different occasions)



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateFullGCCommand.java:
##########
@@ -0,0 +1,416 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.plugins.document.Collection;
+import org.apache.jackrabbit.oak.plugins.document.Document;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.DocumentStoreException;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateFullGCCommand implements Command {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateFullGCCommand.class);
+
+    private static final String USAGE = "generateFullGC {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String FULLGC_GEN_ROOT_PATH = "fullGCGenRoot";
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String FULLGC_GEN_BASE_PATH = "fullGCGenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String FULLGC_GEN_PARENT_NODE_PREFIX = "fullGCParent_";
+    public static String FULLGC_GEN_NODE_PREFIX = "fullGCNode_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node FULLGC_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public GenerateFullGCOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public GenerateFullGCOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {
+        Closer closer = Closer.create();
+        try {
+            execute(closer, args);
+        } catch (Throwable e) {
+            LOG.error("Command failed", e);
+            throw closer.rethrow(e);
+        } finally {
+            closer.close();
+        }
+    }
+
+    /**
+     * Method with passed closer is necessary in order to allow for unit tests to check the output of the command.
+     * It is the responsibility of the caller to close the closer.
+     *
+     * Returns the list of generated garbage base paths (under the garbage root node).
+     * @param closer
+     * @param args
+     * @throws Exception
+     */
+    public List<String> execute(Closer closer, String... args) throws Exception {
+        continuousRunIndex = 0;
+
+        List<String> generateBasePaths = new ArrayList<>();
+
+        GenerateFullGCOptions options = new GenerateFullGCOptions(USAGE).parse(args);
+        String subCmd = options.getSubCmd();
+
+        if (GenerateFullGCOptions.CMD_GENERATE.equals(subCmd)) {
+            if (options.getNumberOfRuns() > 1 && options.getGenerateIntervalSeconds() > 0) {
+                generateBasePaths.addAll(generateGarbageContinuously(options, closer));
+            } else {
+                generateBasePaths.add(generateGarbage(options, closer, 0));
+            }
+        } else if (GenerateFullGCOptions.CMD_CLEAN.equals(subCmd)) {
+            cleanGarbage(options, closer);
+        } else {
+            System.err.println("unknown revisions command: " + subCmd);
+        }
+
+        return generateBasePaths;
+    }
+
+    private List<String> generateGarbageContinuously(GenerateFullGCOptions options, Closer closer) throws IOException, Exception {
+        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long startGenTimestamp = System.currentTimeMillis();
+
+        List<String> generatedGarbageBasePaths = new ArrayList<>();
+
+        int numberOfRuns = options.getNumberOfRuns();
+        int intervalSeconds = options.getGenerateIntervalSeconds();
+        Runnable task = () -> {
+            if (continuousRunIndex < numberOfRuns) {
+                try {
+                    String genBasePath = generateGarbage(options, closer, continuousRunIndex, builder, startGenTimestamp);
+                    generatedGarbageBasePaths.add(genBasePath);
+                } catch (Exception e) {
+                    LOG.error("Error generating garbage in run " + continuousRunIndex, e);
+                }
+                LOG.info("Task executed. Count: " + (continuousRunIndex + 1));
+                continuousRunIndex++;
+            } else {
+                // Shutdown the executor once the task has run numberOfRuns times
+                executor.shutdown();
+                LOG.info("Task completed " + numberOfRuns + " times. Stopping execution.");
+            }
+        };
+
+        // Schedule the task to run every intervalSeconds
+        executor.scheduleAtFixedRate(task, 0, intervalSeconds, TimeUnit.SECONDS);
+
+        return generatedGarbageBasePaths;
+    }
+
+    /**
+     * Generate garbage nodes in the repository in order to allow for testing fullGC functionality.
+     *
+     * Returns the path of the generated FULLGC_GEN_BASE_PATH node (under the root).
+     * @param options
+     * @param closer
+     * @param runIndex
+     * @return
+     * @throws IOException
+     * @throws Exception
+     */
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long generationTimestamp = System.currentTimeMillis();
+
+        return generateGarbage(options, closer, runIndex, builder, generationTimestamp);
+    }
+
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex,
+                                  DocumentNodeStoreBuilder<?> builder, long timestamp) throws IOException, Exception {
+
+        if (builder == null) {
+            System.err.println("generateFullGC mode only available for DocumentNodeStore");
+            System.exit(1);
+        }
+
+        String generationBasePath = FULLGC_GEN_BASE_PATH + timestamp + "_" + runIndex;
+        System.out.println("Generating fullGC on the document: " + generationBasePath);
+        documentNodeStore = builder.build();
+
+        VersionGarbageCollector.FullGCMode fullGCMode = getFullGCMode(options);
+        if (fullGCMode == VersionGarbageCollector.FullGCMode.NONE) {
+            LOG.error("Invalid garbageType specified. Must be one of the following: 1 - EMPTYPROPS, 2 - GAP_ORPHANS, 3 - GAP_ORPHANS_EMPTYPROPS");
+            System.exit(1);
+        }
+
+        //1. Create nodes with properties
+        NodeBuilder rootNode = documentNodeStore.getRoot().builder();
+        NodeBuilder garbageRootNode = rootNode.child(FULLGC_GEN_ROOT_PATH);
+        garbageRootNode.child(generationBasePath).setProperty("jcr:primaryType", "nt:file", NAME);

Review Comment:
   would use `org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE` instead
   
   also, would not use `nt:file` as that requires eg `jcr:content` child etc (which wasn't noticed in the test as the merge happens without commithook - something that's a bit risky, but this is probably an exception as it is primarily a test command - but this aspect isn't ideal and might lead to surprises). we could use `org.apache.jackrabbit.oak.spi.nodetype.NT_OAK_UNSTRUCTURED` instead (as that is unorderable, and unstructured, it would be a good fix as it doesn't have any special requirements otherwise).



##########
oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/VersionGarbageCollector.java:
##########
@@ -168,7 +168,7 @@ public class VersionGarbageCollector {
      * Ultimately the goal is to clean up all possible garbage. After hardening these modes
      * might no longer be supported.
      */
-    enum FullGCMode {
+    public enum FullGCMode {

Review Comment:
   not sure if we want this public? what about adding a helper under `org.apache.jackrabbit.oak.plugins.document` in oak-run instead?
   
   that helper could perhaps also have some `includesEmptyProps` / `includesGapOrphans` etc methods - as fullgc modes is going to be a combination of different modes (and the current check is hard coded to only modes 1 and 2)



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateFullGCCommand.java:
##########
@@ -0,0 +1,416 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.plugins.document.Collection;
+import org.apache.jackrabbit.oak.plugins.document.Document;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.DocumentStoreException;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateFullGCCommand implements Command {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateFullGCCommand.class);
+
+    private static final String USAGE = "generateFullGC {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String FULLGC_GEN_ROOT_PATH = "fullGCGenRoot";
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String FULLGC_GEN_BASE_PATH = "fullGCGenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String FULLGC_GEN_PARENT_NODE_PREFIX = "fullGCParent_";
+    public static String FULLGC_GEN_NODE_PREFIX = "fullGCNode_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node FULLGC_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public GenerateFullGCOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public GenerateFullGCOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {
+        Closer closer = Closer.create();
+        try {
+            execute(closer, args);
+        } catch (Throwable e) {
+            LOG.error("Command failed", e);
+            throw closer.rethrow(e);
+        } finally {
+            closer.close();
+        }
+    }
+
+    /**
+     * Method with passed closer is necessary in order to allow for unit tests to check the output of the command.
+     * It is the responsibility of the caller to close the closer.
+     *
+     * Returns the list of generated garbage base paths (under the garbage root node).
+     * @param closer
+     * @param args
+     * @throws Exception
+     */
+    public List<String> execute(Closer closer, String... args) throws Exception {
+        continuousRunIndex = 0;
+
+        List<String> generateBasePaths = new ArrayList<>();
+
+        GenerateFullGCOptions options = new GenerateFullGCOptions(USAGE).parse(args);
+        String subCmd = options.getSubCmd();
+
+        if (GenerateFullGCOptions.CMD_GENERATE.equals(subCmd)) {
+            if (options.getNumberOfRuns() > 1 && options.getGenerateIntervalSeconds() > 0) {
+                generateBasePaths.addAll(generateGarbageContinuously(options, closer));
+            } else {
+                generateBasePaths.add(generateGarbage(options, closer, 0));
+            }
+        } else if (GenerateFullGCOptions.CMD_CLEAN.equals(subCmd)) {
+            cleanGarbage(options, closer);
+        } else {
+            System.err.println("unknown revisions command: " + subCmd);
+        }
+
+        return generateBasePaths;
+    }
+
+    private List<String> generateGarbageContinuously(GenerateFullGCOptions options, Closer closer) throws IOException, Exception {
+        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long startGenTimestamp = System.currentTimeMillis();
+
+        List<String> generatedGarbageBasePaths = new ArrayList<>();
+
+        int numberOfRuns = options.getNumberOfRuns();
+        int intervalSeconds = options.getGenerateIntervalSeconds();
+        Runnable task = () -> {
+            if (continuousRunIndex < numberOfRuns) {
+                try {
+                    String genBasePath = generateGarbage(options, closer, continuousRunIndex, builder, startGenTimestamp);
+                    generatedGarbageBasePaths.add(genBasePath);
+                } catch (Exception e) {
+                    LOG.error("Error generating garbage in run " + continuousRunIndex, e);
+                }
+                LOG.info("Task executed. Count: " + (continuousRunIndex + 1));
+                continuousRunIndex++;
+            } else {
+                // Shutdown the executor once the task has run numberOfRuns times
+                executor.shutdown();
+                LOG.info("Task completed " + numberOfRuns + " times. Stopping execution.");
+            }
+        };
+
+        // Schedule the task to run every intervalSeconds
+        executor.scheduleAtFixedRate(task, 0, intervalSeconds, TimeUnit.SECONDS);
+
+        return generatedGarbageBasePaths;
+    }
+
+    /**
+     * Generate garbage nodes in the repository in order to allow for testing fullGC functionality.
+     *
+     * Returns the path of the generated FULLGC_GEN_BASE_PATH node (under the root).
+     * @param options
+     * @param closer
+     * @param runIndex
+     * @return
+     * @throws IOException
+     * @throws Exception
+     */
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long generationTimestamp = System.currentTimeMillis();
+
+        return generateGarbage(options, closer, runIndex, builder, generationTimestamp);
+    }
+
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex,
+                                  DocumentNodeStoreBuilder<?> builder, long timestamp) throws IOException, Exception {
+
+        if (builder == null) {
+            System.err.println("generateFullGC mode only available for DocumentNodeStore");
+            System.exit(1);
+        }
+
+        String generationBasePath = FULLGC_GEN_BASE_PATH + timestamp + "_" + runIndex;
+        System.out.println("Generating fullGC on the document: " + generationBasePath);
+        documentNodeStore = builder.build();
+
+        VersionGarbageCollector.FullGCMode fullGCMode = getFullGCMode(options);
+        if (fullGCMode == VersionGarbageCollector.FullGCMode.NONE) {
+            LOG.error("Invalid garbageType specified. Must be one of the following: 1 - EMPTYPROPS, 2 - GAP_ORPHANS, 3 - GAP_ORPHANS_EMPTYPROPS");
+            System.exit(1);
+        }
+
+        //1. Create nodes with properties
+        NodeBuilder rootNode = documentNodeStore.getRoot().builder();
+        NodeBuilder garbageRootNode = rootNode.child(FULLGC_GEN_ROOT_PATH);
+        garbageRootNode.child(generationBasePath).setProperty("jcr:primaryType", "nt:file", NAME);
+
+        int nodesCountUnderParent = options.getCreateGarbageNodesCount() / options.getGarbageNodesParentCount();
+        for(int i = 0; i < options.getGarbageNodesParentCount(); i ++) {
+            garbageRootNode.child(generationBasePath).child(FULLGC_GEN_PARENT_NODE_PREFIX + i).setProperty("jcr:primaryType", "nt:folder", NAME);
+
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                garbageRootNode.child(generationBasePath).child(FULLGC_GEN_PARENT_NODE_PREFIX + i).child(FULLGC_GEN_NODE_PREFIX + j).
+                        setProperty("jcr:primaryType", "nt:file", NAME);
+
+                if (fullGCMode == VersionGarbageCollector.FullGCMode.EMPTYPROPS || fullGCMode == VersionGarbageCollector.FullGCMode.GAP_ORPHANS_EMPTYPROPS) {
+                    garbageRootNode.child(generationBasePath).child(FULLGC_GEN_PARENT_NODE_PREFIX + i).child(FULLGC_GEN_NODE_PREFIX + j).

Review Comment:
   what might be interesting is adding different types of "child subtrees". IIUC then the it will delete the nodes at level `FULLGC_GEN_PARENT_NODE_PREFIX` (which then leaves its children gap orphans). currently it only creates a 1-level deep child under that node. what would be interesting is to have different cases:
   * (later perhaps) 0-level deep but just split docs (we actually probably will miss that type of garbage, that's going to be a new TODO probably, but this test could trigger it)
   * 1-level deep (which you already have)
   * 2-level deep
   * 3-level deep
   
   reason being that it might cause different behavior in fullGC code later on - and thus having slight variations of that garbage could be useful to have



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateFullGCCommand.java:
##########
@@ -0,0 +1,416 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.plugins.document.Collection;
+import org.apache.jackrabbit.oak.plugins.document.Document;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.DocumentStoreException;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateFullGCCommand implements Command {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateFullGCCommand.class);
+
+    private static final String USAGE = "generateFullGC {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String FULLGC_GEN_ROOT_PATH = "fullGCGenRoot";
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String FULLGC_GEN_BASE_PATH = "fullGCGenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String FULLGC_GEN_PARENT_NODE_PREFIX = "fullGCParent_";
+    public static String FULLGC_GEN_NODE_PREFIX = "fullGCNode_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node FULLGC_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public GenerateFullGCOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public GenerateFullGCOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {
+        Closer closer = Closer.create();
+        try {
+            execute(closer, args);
+        } catch (Throwable e) {
+            LOG.error("Command failed", e);
+            throw closer.rethrow(e);
+        } finally {
+            closer.close();
+        }
+    }
+
+    /**
+     * Method with passed closer is necessary in order to allow for unit tests to check the output of the command.
+     * It is the responsibility of the caller to close the closer.
+     *
+     * Returns the list of generated garbage base paths (under the garbage root node).
+     * @param closer
+     * @param args
+     * @throws Exception
+     */
+    public List<String> execute(Closer closer, String... args) throws Exception {
+        continuousRunIndex = 0;
+
+        List<String> generateBasePaths = new ArrayList<>();
+
+        GenerateFullGCOptions options = new GenerateFullGCOptions(USAGE).parse(args);
+        String subCmd = options.getSubCmd();
+
+        if (GenerateFullGCOptions.CMD_GENERATE.equals(subCmd)) {
+            if (options.getNumberOfRuns() > 1 && options.getGenerateIntervalSeconds() > 0) {
+                generateBasePaths.addAll(generateGarbageContinuously(options, closer));
+            } else {
+                generateBasePaths.add(generateGarbage(options, closer, 0));
+            }
+        } else if (GenerateFullGCOptions.CMD_CLEAN.equals(subCmd)) {
+            cleanGarbage(options, closer);
+        } else {
+            System.err.println("unknown revisions command: " + subCmd);
+        }
+
+        return generateBasePaths;
+    }
+
+    private List<String> generateGarbageContinuously(GenerateFullGCOptions options, Closer closer) throws IOException, Exception {
+        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long startGenTimestamp = System.currentTimeMillis();
+
+        List<String> generatedGarbageBasePaths = new ArrayList<>();
+
+        int numberOfRuns = options.getNumberOfRuns();
+        int intervalSeconds = options.getGenerateIntervalSeconds();
+        Runnable task = () -> {
+            if (continuousRunIndex < numberOfRuns) {
+                try {
+                    String genBasePath = generateGarbage(options, closer, continuousRunIndex, builder, startGenTimestamp);
+                    generatedGarbageBasePaths.add(genBasePath);
+                } catch (Exception e) {
+                    LOG.error("Error generating garbage in run " + continuousRunIndex, e);
+                }
+                LOG.info("Task executed. Count: " + (continuousRunIndex + 1));
+                continuousRunIndex++;
+            } else {
+                // Shutdown the executor once the task has run numberOfRuns times
+                executor.shutdown();
+                LOG.info("Task completed " + numberOfRuns + " times. Stopping execution.");
+            }
+        };
+
+        // Schedule the task to run every intervalSeconds
+        executor.scheduleAtFixedRate(task, 0, intervalSeconds, TimeUnit.SECONDS);
+
+        return generatedGarbageBasePaths;
+    }
+
+    /**
+     * Generate garbage nodes in the repository in order to allow for testing fullGC functionality.
+     *
+     * Returns the path of the generated FULLGC_GEN_BASE_PATH node (under the root).
+     * @param options
+     * @param closer
+     * @param runIndex
+     * @return
+     * @throws IOException
+     * @throws Exception
+     */
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long generationTimestamp = System.currentTimeMillis();
+
+        return generateGarbage(options, closer, runIndex, builder, generationTimestamp);
+    }
+
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex,
+                                  DocumentNodeStoreBuilder<?> builder, long timestamp) throws IOException, Exception {
+
+        if (builder == null) {
+            System.err.println("generateFullGC mode only available for DocumentNodeStore");
+            System.exit(1);
+        }
+
+        String generationBasePath = FULLGC_GEN_BASE_PATH + timestamp + "_" + runIndex;
+        System.out.println("Generating fullGC on the document: " + generationBasePath);
+        documentNodeStore = builder.build();
+
+        VersionGarbageCollector.FullGCMode fullGCMode = getFullGCMode(options);
+        if (fullGCMode == VersionGarbageCollector.FullGCMode.NONE) {
+            LOG.error("Invalid garbageType specified. Must be one of the following: 1 - EMPTYPROPS, 2 - GAP_ORPHANS, 3 - GAP_ORPHANS_EMPTYPROPS");

Review Comment:
   assume this log message will be outdated soon with more fullGC modes ready for rollout. eg we will support ALL_ORPHANS at some point etc... Perhaps just mention to use a valid fullGc mode (and perhaps add the classname)?



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateFullGCCommand.java:
##########
@@ -0,0 +1,416 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.plugins.document.Collection;
+import org.apache.jackrabbit.oak.plugins.document.Document;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.DocumentStoreException;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateFullGCCommand implements Command {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateFullGCCommand.class);
+
+    private static final String USAGE = "generateFullGC {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String FULLGC_GEN_ROOT_PATH = "fullGCGenRoot";
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String FULLGC_GEN_BASE_PATH = "fullGCGenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String FULLGC_GEN_PARENT_NODE_PREFIX = "fullGCParent_";
+    public static String FULLGC_GEN_NODE_PREFIX = "fullGCNode_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node FULLGC_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public GenerateFullGCOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public GenerateFullGCOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {
+        Closer closer = Closer.create();
+        try {
+            execute(closer, args);
+        } catch (Throwable e) {
+            LOG.error("Command failed", e);
+            throw closer.rethrow(e);
+        } finally {
+            closer.close();
+        }
+    }
+
+    /**
+     * Method with passed closer is necessary in order to allow for unit tests to check the output of the command.
+     * It is the responsibility of the caller to close the closer.
+     *
+     * Returns the list of generated garbage base paths (under the garbage root node).
+     * @param closer
+     * @param args
+     * @throws Exception
+     */
+    public List<String> execute(Closer closer, String... args) throws Exception {
+        continuousRunIndex = 0;
+
+        List<String> generateBasePaths = new ArrayList<>();
+
+        GenerateFullGCOptions options = new GenerateFullGCOptions(USAGE).parse(args);
+        String subCmd = options.getSubCmd();
+
+        if (GenerateFullGCOptions.CMD_GENERATE.equals(subCmd)) {
+            if (options.getNumberOfRuns() > 1 && options.getGenerateIntervalSeconds() > 0) {
+                generateBasePaths.addAll(generateGarbageContinuously(options, closer));
+            } else {
+                generateBasePaths.add(generateGarbage(options, closer, 0));
+            }
+        } else if (GenerateFullGCOptions.CMD_CLEAN.equals(subCmd)) {
+            cleanGarbage(options, closer);
+        } else {
+            System.err.println("unknown revisions command: " + subCmd);
+        }
+
+        return generateBasePaths;
+    }
+
+    private List<String> generateGarbageContinuously(GenerateFullGCOptions options, Closer closer) throws IOException, Exception {
+        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long startGenTimestamp = System.currentTimeMillis();
+
+        List<String> generatedGarbageBasePaths = new ArrayList<>();
+
+        int numberOfRuns = options.getNumberOfRuns();
+        int intervalSeconds = options.getGenerateIntervalSeconds();
+        Runnable task = () -> {
+            if (continuousRunIndex < numberOfRuns) {
+                try {
+                    String genBasePath = generateGarbage(options, closer, continuousRunIndex, builder, startGenTimestamp);
+                    generatedGarbageBasePaths.add(genBasePath);
+                } catch (Exception e) {
+                    LOG.error("Error generating garbage in run " + continuousRunIndex, e);
+                }
+                LOG.info("Task executed. Count: " + (continuousRunIndex + 1));
+                continuousRunIndex++;
+            } else {
+                // Shutdown the executor once the task has run numberOfRuns times
+                executor.shutdown();
+                LOG.info("Task completed " + numberOfRuns + " times. Stopping execution.");
+            }
+        };
+
+        // Schedule the task to run every intervalSeconds
+        executor.scheduleAtFixedRate(task, 0, intervalSeconds, TimeUnit.SECONDS);
+
+        return generatedGarbageBasePaths;
+    }
+
+    /**
+     * Generate garbage nodes in the repository in order to allow for testing fullGC functionality.
+     *
+     * Returns the path of the generated FULLGC_GEN_BASE_PATH node (under the root).
+     * @param options
+     * @param closer
+     * @param runIndex
+     * @return
+     * @throws IOException
+     * @throws Exception
+     */
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long generationTimestamp = System.currentTimeMillis();
+
+        return generateGarbage(options, closer, runIndex, builder, generationTimestamp);
+    }
+
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex,
+                                  DocumentNodeStoreBuilder<?> builder, long timestamp) throws IOException, Exception {
+
+        if (builder == null) {
+            System.err.println("generateFullGC mode only available for DocumentNodeStore");
+            System.exit(1);
+        }
+
+        String generationBasePath = FULLGC_GEN_BASE_PATH + timestamp + "_" + runIndex;
+        System.out.println("Generating fullGC on the document: " + generationBasePath);
+        documentNodeStore = builder.build();
+
+        VersionGarbageCollector.FullGCMode fullGCMode = getFullGCMode(options);
+        if (fullGCMode == VersionGarbageCollector.FullGCMode.NONE) {
+            LOG.error("Invalid garbageType specified. Must be one of the following: 1 - EMPTYPROPS, 2 - GAP_ORPHANS, 3 - GAP_ORPHANS_EMPTYPROPS");
+            System.exit(1);
+        }
+
+        //1. Create nodes with properties
+        NodeBuilder rootNode = documentNodeStore.getRoot().builder();
+        NodeBuilder garbageRootNode = rootNode.child(FULLGC_GEN_ROOT_PATH);
+        garbageRootNode.child(generationBasePath).setProperty("jcr:primaryType", "nt:file", NAME);
+
+        int nodesCountUnderParent = options.getCreateGarbageNodesCount() / options.getGarbageNodesParentCount();
+        for(int i = 0; i < options.getGarbageNodesParentCount(); i ++) {
+            garbageRootNode.child(generationBasePath).child(FULLGC_GEN_PARENT_NODE_PREFIX + i).setProperty("jcr:primaryType", "nt:folder", NAME);
+
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                garbageRootNode.child(generationBasePath).child(FULLGC_GEN_PARENT_NODE_PREFIX + i).child(FULLGC_GEN_NODE_PREFIX + j).
+                        setProperty("jcr:primaryType", "nt:file", NAME);
+
+                if (fullGCMode == VersionGarbageCollector.FullGCMode.EMPTYPROPS || fullGCMode == VersionGarbageCollector.FullGCMode.GAP_ORPHANS_EMPTYPROPS) {
+                    garbageRootNode.child(generationBasePath).child(FULLGC_GEN_PARENT_NODE_PREFIX + i).child(FULLGC_GEN_NODE_PREFIX + j).
+                            setProperty(EMPTY_PROPERTY_NAME, "bar", STRING);
+                }
+            }
+        }
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+
+
+        //2. Generate garbage nodes - EMPTY_PROPERTIES
+        if (fullGCMode == VersionGarbageCollector.FullGCMode.EMPTYPROPS) {
+            for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+                for (int j = 0; j < nodesCountUnderParent; j++) {
+                    garbageRootNode.child(generationBasePath).child(FULLGC_GEN_PARENT_NODE_PREFIX + i).child(FULLGC_GEN_NODE_PREFIX + j).
+                            removeProperty(EMPTY_PROPERTY_NAME);
+                }
+            }
+        }
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+
+        //3.1. Generate garbage nodes - GAP_ORPHANS - remove parent nodes
+        if (fullGCMode == VersionGarbageCollector.FullGCMode.GAP_ORPHANS) {
+            StringBuilder sbNodePath = new StringBuilder();
+            List<String> deleteNodePaths = new ArrayList<>();
+            for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+
+                sbNodePath.setLength(0);
+                sbNodePath.append("3:/").append(FULLGC_GEN_ROOT_PATH).append("/").append(generationBasePath).append("/").

Review Comment:
   (same suggestion here to use Utils for path composition and id conversion)



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "shodaaan (via GitHub)" <gi...@apache.org>.
shodaaan commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1809013965


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateFullGCCommand.java:
##########
@@ -0,0 +1,416 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.plugins.document.Collection;
+import org.apache.jackrabbit.oak.plugins.document.Document;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.DocumentStoreException;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateFullGCCommand implements Command {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateFullGCCommand.class);
+
+    private static final String USAGE = "generateFullGC {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String FULLGC_GEN_ROOT_PATH = "fullGCGenRoot";
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String FULLGC_GEN_BASE_PATH = "fullGCGenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String FULLGC_GEN_PARENT_NODE_PREFIX = "fullGCParent_";
+    public static String FULLGC_GEN_NODE_PREFIX = "fullGCNode_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node FULLGC_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public GenerateFullGCOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public GenerateFullGCOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {
+        Closer closer = Closer.create();
+        try {
+            execute(closer, args);
+        } catch (Throwable e) {
+            LOG.error("Command failed", e);
+            throw closer.rethrow(e);
+        } finally {
+            closer.close();
+        }
+    }
+
+    /**
+     * Method with passed closer is necessary in order to allow for unit tests to check the output of the command.
+     * It is the responsibility of the caller to close the closer.
+     *
+     * Returns the list of generated garbage base paths (under the garbage root node).
+     * @param closer
+     * @param args
+     * @throws Exception
+     */
+    public List<String> execute(Closer closer, String... args) throws Exception {
+        continuousRunIndex = 0;
+
+        List<String> generateBasePaths = new ArrayList<>();
+
+        GenerateFullGCOptions options = new GenerateFullGCOptions(USAGE).parse(args);
+        String subCmd = options.getSubCmd();
+
+        if (GenerateFullGCOptions.CMD_GENERATE.equals(subCmd)) {
+            if (options.getNumberOfRuns() > 1 && options.getGenerateIntervalSeconds() > 0) {
+                generateBasePaths.addAll(generateGarbageContinuously(options, closer));
+            } else {
+                generateBasePaths.add(generateGarbage(options, closer, 0));
+            }
+        } else if (GenerateFullGCOptions.CMD_CLEAN.equals(subCmd)) {
+            cleanGarbage(options, closer);
+        } else {
+            System.err.println("unknown revisions command: " + subCmd);
+        }
+
+        return generateBasePaths;
+    }
+
+    private List<String> generateGarbageContinuously(GenerateFullGCOptions options, Closer closer) throws IOException, Exception {

Review Comment:
   Removed IOException, just Exception is thrown now.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "shodaaan (via GitHub)" <gi...@apache.org>.
shodaaan commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1809008948


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateFullGCCommand.java:
##########
@@ -0,0 +1,416 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.plugins.document.Collection;
+import org.apache.jackrabbit.oak.plugins.document.Document;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.DocumentStoreException;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateFullGCCommand implements Command {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateFullGCCommand.class);
+
+    private static final String USAGE = "generateFullGC {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String FULLGC_GEN_ROOT_PATH = "fullGCGenRoot";

Review Comment:
   Made both changes. Thank you for the suggestion.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "shodaaan (via GitHub)" <gi...@apache.org>.
shodaaan commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1809020297


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateFullGCCommand.java:
##########
@@ -0,0 +1,416 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.plugins.document.Collection;
+import org.apache.jackrabbit.oak.plugins.document.Document;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.DocumentStoreException;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateFullGCCommand implements Command {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateFullGCCommand.class);
+
+    private static final String USAGE = "generateFullGC {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String FULLGC_GEN_ROOT_PATH = "fullGCGenRoot";
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String FULLGC_GEN_BASE_PATH = "fullGCGenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String FULLGC_GEN_PARENT_NODE_PREFIX = "fullGCParent_";
+    public static String FULLGC_GEN_NODE_PREFIX = "fullGCNode_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node FULLGC_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public GenerateFullGCOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public GenerateFullGCOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {
+        Closer closer = Closer.create();
+        try {
+            execute(closer, args);
+        } catch (Throwable e) {
+            LOG.error("Command failed", e);
+            throw closer.rethrow(e);
+        } finally {
+            closer.close();
+        }
+    }
+
+    /**
+     * Method with passed closer is necessary in order to allow for unit tests to check the output of the command.
+     * It is the responsibility of the caller to close the closer.
+     *
+     * Returns the list of generated garbage base paths (under the garbage root node).
+     * @param closer
+     * @param args
+     * @throws Exception
+     */
+    public List<String> execute(Closer closer, String... args) throws Exception {
+        continuousRunIndex = 0;
+
+        List<String> generateBasePaths = new ArrayList<>();
+
+        GenerateFullGCOptions options = new GenerateFullGCOptions(USAGE).parse(args);
+        String subCmd = options.getSubCmd();
+
+        if (GenerateFullGCOptions.CMD_GENERATE.equals(subCmd)) {
+            if (options.getNumberOfRuns() > 1 && options.getGenerateIntervalSeconds() > 0) {
+                generateBasePaths.addAll(generateGarbageContinuously(options, closer));
+            } else {
+                generateBasePaths.add(generateGarbage(options, closer, 0));
+            }
+        } else if (GenerateFullGCOptions.CMD_CLEAN.equals(subCmd)) {
+            cleanGarbage(options, closer);
+        } else {
+            System.err.println("unknown revisions command: " + subCmd);
+        }
+
+        return generateBasePaths;
+    }
+
+    private List<String> generateGarbageContinuously(GenerateFullGCOptions options, Closer closer) throws IOException, Exception {
+        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long startGenTimestamp = System.currentTimeMillis();
+
+        List<String> generatedGarbageBasePaths = new ArrayList<>();
+
+        int numberOfRuns = options.getNumberOfRuns();
+        int intervalSeconds = options.getGenerateIntervalSeconds();
+        Runnable task = () -> {
+            if (continuousRunIndex < numberOfRuns) {
+                try {
+                    String genBasePath = generateGarbage(options, closer, continuousRunIndex, builder, startGenTimestamp);
+                    generatedGarbageBasePaths.add(genBasePath);
+                } catch (Exception e) {
+                    LOG.error("Error generating garbage in run " + continuousRunIndex, e);
+                }
+                LOG.info("Task executed. Count: " + (continuousRunIndex + 1));
+                continuousRunIndex++;
+            } else {
+                // Shutdown the executor once the task has run numberOfRuns times
+                executor.shutdown();
+                LOG.info("Task completed " + numberOfRuns + " times. Stopping execution.");
+            }
+        };
+
+        // Schedule the task to run every intervalSeconds
+        executor.scheduleAtFixedRate(task, 0, intervalSeconds, TimeUnit.SECONDS);
+
+        return generatedGarbageBasePaths;
+    }
+
+    /**
+     * Generate garbage nodes in the repository in order to allow for testing fullGC functionality.
+     *
+     * Returns the path of the generated FULLGC_GEN_BASE_PATH node (under the root).
+     * @param options
+     * @param closer
+     * @param runIndex
+     * @return
+     * @throws IOException
+     * @throws Exception
+     */
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long generationTimestamp = System.currentTimeMillis();
+
+        return generateGarbage(options, closer, runIndex, builder, generationTimestamp);
+    }
+
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex,
+                                  DocumentNodeStoreBuilder<?> builder, long timestamp) throws IOException, Exception {

Review Comment:
   The timestamp is necessary for the flow of calling generateGarbage from generateGarbageContinuously, where all of the generated garbage must be under the parent with the initial timestamp.
   No change made.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "shodaaan (via GitHub)" <gi...@apache.org>.
shodaaan commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1820292943


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/AvailableModes.java:
##########
@@ -75,5 +75,6 @@ public final class AvailableModes {
             .put("segment-copy", new SegmentCopyCommand())
             .put("server", new ServerCommand())
             .put("purge-index-versions", new LucenePurgeOldIndexVersionCommand())
+            .put("generateGarbage", new GenerateGarbageCommand())

Review Comment:
   Changed, thank you for the suggestion.



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateGarbageCommand.java:
##########
@@ -0,0 +1,553 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.commons.concurrent.ExecutorCloser;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.GenerateGarbageHelper;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateGarbageCommand implements Command, Closeable {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateGarbageCommand.class);
+
+    private final ScheduledExecutorService continuousRunExecutor = Executors.newScheduledThreadPool(1);
+
+    private static final String USAGE = "createGarbage {<jdbc-uri> | <mongodb-uri>} [options]";

Review Comment:
   Changed, thank you for the suggestion.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "rishabhdaim (via GitHub)" <gi...@apache.org>.
rishabhdaim commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1831399984


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/CreateGarbageCommand.java:
##########
@@ -0,0 +1,574 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.base.Joiner;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.commons.concurrent.ExecutorCloser;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.GenerateGarbageHelper;
+import org.apache.jackrabbit.oak.plugins.document.LeaseCheckMode;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.asCloseable;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateGarbageCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class CreateGarbageCommand implements Command, Closeable {
+    private static final Logger LOG = LoggerFactory.getLogger(CreateGarbageCommand.class);
+
+    private final ScheduledExecutorService continuousRunExecutor = Executors.newScheduledThreadPool(1);
+
+    private static final String USAGE = Joiner.on(System.lineSeparator()).join(
+            "create-test-garbage {<jdbc-uri> | <mongodb-uri>} <sub-command> [options] ",
+            "where sub-command is one of: ",
+            "  clean - clean up all generated garbage under the specified root node",
+            "  generate - generate garbage nodes in the repository, under the root node tmp/oak-run-generated-test-garbage. ",
+            "           Use the --garbageType (required) option to specify the type of garbage to generate",
+            "           the --garbageNodesCount option to specify the total number of garbage nodes to create,",
+            "           the --garbageNodesParentCount option to specify the total number of parent nodes under which to create garbage nodes,",
+            "           the --orphansDepth option to specify the depth in the tree at which to create garbage nodes,",
+            "           the --orphansLevelGap option to specify the gap in the tree between the first leaf gap orphan garbage node and its parent,",
+            "           the --numberOfRuns option to specify the number of garbage generation runs to do,",
+            "           and the --generateIntervalSeconds option to specify the interval at which to generate a complete garbage count from createGarbageNotesCount.");
+
+
+    /**
+     * Garbage should be generated under tmp node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH_BASE = "tmp";
+
+    /**
+     * The root node name for garbage generation, one level under tmp.
+     */
+    public static String GARBAGE_GEN_ROOT_NODE_NAME = "oak-run-generated-test-garbage";
+
+    /**
+     * Root node for garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH = GARBAGE_GEN_ROOT_PATH_BASE + "/" + GARBAGE_GEN_ROOT_NODE_NAME;
+
+    /**
+     * Base path for garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String GEN_BASE_PATH = "GenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String GEN_PARENT_NODE_PREFIX = "Parent_";
+    public static String GEN_NODE_PREFIX = "Node_";
+    public static String GEN_NODE_LEVEL_PREFIX = "_Level_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    /**
+     * The maximum depth in the tree at which to generate gap orphans garbage nodes.
+     * Used for validation of the orphansDepth / orphansLevelGap options.
+     */
+    private final int GAP_ORPHANS_MAX_DEPTH = 15;
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class CreateGarbageOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node GARBAGE_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> orphansDepth;
+        final OptionSpec<Integer> orphansLevelGap;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public CreateGarbageOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            orphansDepth = parser
+                    .accepts("orphansDepth", "the depth in the tree at which to create garbage nodes. Only applies to ORPHANS fullGC modes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            orphansLevelGap = parser
+                    .accepts("orphansLevelGap", "the gap in the tree between the first leaf gap orphan garbage node and its' parent. Only applies to ORPHANS fullGC modes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public CreateGarbageOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getOrphansDepth() { return orphansDepth.value(options);}
+
+        public int getOrphansLevelGap() { return orphansLevelGap.value(options); }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {
+
+        try (Closer closer = Closer.create()) {
+            // Register the ExecutorService with Closer
+            closer.register(() -> {
+                // Properly shutdown the executor service
+                continuousRunExecutor.shutdown();
+                try {
+                    if (!continuousRunExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
+                        continuousRunExecutor.shutdownNow();
+                    }
+                } catch (InterruptedException e) {
+                    throw new RuntimeException(e);
+                }
+            });
+
+            execute(closer, args);
+
+        } catch (Throwable e) {
+            LOG.error("Command failed", e);
+            throw e;
+        }
+    }
+
+    /**
+     * Method with passed closer is necessary in order to allow for unit tests to check the output of the command.
+     * It is the responsibility of the caller to close the closer.
+     *
+     * Returns the list of generated garbage base paths (under the garbage root node).
+     * @param closer
+     * @param args
+     * @throws Exception
+     */
+    public List<String> execute(Closer closer, String... args) throws Exception {
+        continuousRunIndex = 0;
+
+        List<String> generateBasePaths = new ArrayList<>();
+
+        CreateGarbageOptions options = new CreateGarbageOptions(USAGE).parse(args);
+        String subCmd = options.getSubCmd();
+
+        LOG.info("Executing command: " + getClass().getName() + " with sub-command: " + subCmd);
+
+        if (CreateGarbageOptions.CMD_GENERATE.equals(subCmd)) {
+
+            if (GenerateGarbageHelper.isInvalidGarbageGenerationMode(options.getGarbageType())) {
+                LOG.error("Invalid fullGCMode specified: " + options.getGarbageType() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+            if (options.getCreateGarbageNodesCount() <= 0) {
+                LOG.error("Invalid garbageNodesCount specified: " + options.getCreateGarbageNodesCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+            if (options.getGarbageNodesParentCount() <= 0) {
+                LOG.error("Invalid garbageNodesParentCount specified: " + options.getGarbageNodesParentCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+
+            if (options.getNumberOfRuns() > 1 && options.getGenerateIntervalSeconds() > 0) {
+                generateBasePaths.addAll(generateGarbageContinuously(options, closer));
+            } else {
+                generateBasePaths.add(createGarbage(options, closer, 0));
+            }
+        } else if (CreateGarbageOptions.CMD_CLEAN.equals(subCmd)) {
+            cleanGarbage(options, closer);
+        } else {
+            System.err.println("Unknown revisions command: " + subCmd);
+        }
+
+        return generateBasePaths;
+    }
+
+    @Override
+    public void close() throws IOException {

Review Comment:
   We can safely remove this since we are registering `continuousRunExecutor` with a `closer` instance.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "rishabhdaim (via GitHub)" <gi...@apache.org>.
rishabhdaim commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1831395200


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/CreateGarbageCommand.java:
##########
@@ -0,0 +1,574 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.base.Joiner;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.commons.concurrent.ExecutorCloser;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.GenerateGarbageHelper;
+import org.apache.jackrabbit.oak.plugins.document.LeaseCheckMode;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.asCloseable;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateGarbageCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class CreateGarbageCommand implements Command, Closeable {
+    private static final Logger LOG = LoggerFactory.getLogger(CreateGarbageCommand.class);
+
+    private final ScheduledExecutorService continuousRunExecutor = Executors.newScheduledThreadPool(1);
+
+    private static final String USAGE = Joiner.on(System.lineSeparator()).join(
+            "create-test-garbage {<jdbc-uri> | <mongodb-uri>} <sub-command> [options] ",
+            "where sub-command is one of: ",
+            "  clean - clean up all generated garbage under the specified root node",
+            "  generate - generate garbage nodes in the repository, under the root node tmp/oak-run-generated-test-garbage. ",
+            "           Use the --garbageType (required) option to specify the type of garbage to generate",
+            "           the --garbageNodesCount option to specify the total number of garbage nodes to create,",
+            "           the --garbageNodesParentCount option to specify the total number of parent nodes under which to create garbage nodes,",
+            "           the --orphansDepth option to specify the depth in the tree at which to create garbage nodes,",
+            "           the --orphansLevelGap option to specify the gap in the tree between the first leaf gap orphan garbage node and its parent,",
+            "           the --numberOfRuns option to specify the number of garbage generation runs to do,",
+            "           and the --generateIntervalSeconds option to specify the interval at which to generate a complete garbage count from createGarbageNotesCount.");
+
+
+    /**
+     * Garbage should be generated under tmp node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH_BASE = "tmp";
+
+    /**
+     * The root node name for garbage generation, one level under tmp.
+     */
+    public static String GARBAGE_GEN_ROOT_NODE_NAME = "oak-run-generated-test-garbage";
+
+    /**
+     * Root node for garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH = GARBAGE_GEN_ROOT_PATH_BASE + "/" + GARBAGE_GEN_ROOT_NODE_NAME;
+
+    /**
+     * Base path for garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String GEN_BASE_PATH = "GenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String GEN_PARENT_NODE_PREFIX = "Parent_";
+    public static String GEN_NODE_PREFIX = "Node_";
+    public static String GEN_NODE_LEVEL_PREFIX = "_Level_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    /**
+     * The maximum depth in the tree at which to generate gap orphans garbage nodes.
+     * Used for validation of the orphansDepth / orphansLevelGap options.
+     */
+    private final int GAP_ORPHANS_MAX_DEPTH = 15;
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class CreateGarbageOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node GARBAGE_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> orphansDepth;
+        final OptionSpec<Integer> orphansLevelGap;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public CreateGarbageOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            orphansDepth = parser
+                    .accepts("orphansDepth", "the depth in the tree at which to create garbage nodes. Only applies to ORPHANS fullGC modes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            orphansLevelGap = parser
+                    .accepts("orphansLevelGap", "the gap in the tree between the first leaf gap orphan garbage node and its' parent. Only applies to ORPHANS fullGC modes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public CreateGarbageOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getOrphansDepth() { return orphansDepth.value(options);}
+
+        public int getOrphansLevelGap() { return orphansLevelGap.value(options); }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {

Review Comment:
   missing `@Override` annotation.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "rishabhdaim (via GitHub)" <gi...@apache.org>.
rishabhdaim commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1831376592


##########
oak-run/src/test/java/org/apache/jackrabbit/oak/plugins/document/CreateGarbageCommandTest.java:
##########
@@ -0,0 +1,437 @@
+/*
+ * 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 org.apache.jackrabbit.guava.common.collect.ImmutableList;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.plugins.document.util.MongoConnection;
+import org.apache.jackrabbit.oak.plugins.document.util.Utils;
+import org.apache.jackrabbit.oak.run.CreateGarbageCommand;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.apache.jackrabbit.oak.plugins.document.CommandTestUtils.captureSystemOut;
+import static org.apache.jackrabbit.oak.run.CreateGarbageCommand.EMPTY_PROPERTY_NAME;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+public class CreateGarbageCommandTest {
+
+    final String OPTION_GARBAGE_NODES_COUNT = "--garbageNodesCount";
+    final String OPTION_GARBAGE_NODES_PARENT_COUNT = "--garbageNodesParentCount";
+    final String OPTION_GARBAGE_TYPE = "--garbageType";
+    final String OPTION_GARBAGE_ORPHANS_DEPTH = "--orphansDepth";
+    final String OPTION_GARBAGE_ORPHANS_LEVEL_GAP = "--orphansLevelGap";
+    final String OPTION_NUMBER_OF_RUNS = "--numberOfRuns";
+    final String OPTION_GENERATE_INTERVAL_SECONDS = "--generateIntervalSeconds";
+
+    @Rule
+    public MongoConnectionFactory connectionFactory = new MongoConnectionFactory();
+
+    @Rule
+    public DocumentMKBuilderProvider builderProvider = new DocumentMKBuilderProvider();
+
+    private DocumentNodeStore ns;
+
+    private Closer closer;
+
+    @BeforeClass
+    public static void assumeMongoDB() {
+        assumeTrue(MongoUtils.isAvailable());
+    }
+
+    @Before
+    public void before() {
+        ns = createDocumentNodeStore();
+    }
+
+    @After
+    public void after() {
+        try {
+            closer.close();
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+        ns.dispose();
+    }
+
+    private DocumentNodeStore createDocumentNodeStore() {
+        MongoConnection c = connectionFactory.getConnection();
+        assertNotNull(c);
+        MongoUtils.dropCollections(c.getDatabase());
+        return builderProvider.newBuilder().setFullGCEnabled(false).setLeaseCheckMode(LeaseCheckMode.DISABLED).setAsyncDelay(0)
+                .setMongoDB(c.getMongoClient(), c.getDBName()).getNodeStore();
+    }
+
+    private List<String> getChildNodeNames(NodeState node) {
+        List<String> childNodeNames = new ArrayList<>();
+        for (String childNodeName : node.getChildNodeNames()) {
+            childNodeNames.add(childNodeName);
+        }
+        return childNodeNames;
+    }
+
+    /**
+     * This test generates garbage empty properties under one parent node and then cleans up the garbage.
+     * Use a custom MongoDB connection string for the MongoDB to run the test on.
+     * Long running test, should only be run manually.
+     */
+    @Ignore
+    @Test
+    public void generateGarbageEmptyPropsUnderOneParentOnCustomMongoDB() {
+        ns.dispose();
+
+        // this should be a valid mongoDB connection string for the MongoDB on which to generate the garbage
+        String mongoDBConnString = "";
+        CreateGarbageCmd cmd = new CreateGarbageCmd( mongoDBConnString, OPTION_GARBAGE_NODES_COUNT, "10", OPTION_GARBAGE_NODES_PARENT_COUNT, "1",
+                OPTION_GARBAGE_TYPE, "1");
+        String output = captureSystemOut(cmd);
+
+        //sleep thread for 10 seconds to allow the garbage generation to complete
+        try {
+            Thread.sleep(10000);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+
+        DocumentNodeStore nodeStore = cmd.getCommand().getDocumentNodeStore();
+        NodeState garbageRootNodeState = getGarbageRootNode(nodeStore);
+        List<String> garbageRootNodeChildNames = getChildNodeNames(garbageRootNodeState);
+
+        assertEquals(garbageRootNodeChildNames.size(), 1);
+        List<String> propParentsGarbageNodeNames = getChildNodeNames(garbageRootNodeState.getChildNode(garbageRootNodeChildNames.get(0)));
+        assertEquals(propParentsGarbageNodeNames.size(), 1);
+
+        List<String> propGarbageNodeNames = getChildNodeNames(garbageRootNodeState.getChildNode(garbageRootNodeChildNames.get(0)).getChildNode(propParentsGarbageNodeNames.get(0)));
+        assertEquals(propGarbageNodeNames.size(), 10);
+    }
+
+    /**
+     * This test generates garbage empty properties and then cleans up the garbage.
+     * Long running test, should only be run manually.
+     */
+    @Ignore
+    @Test
+    public void cleanupAllGarbageEmptyProperties() {
+
+        // generate garbage first
+        CreateGarbageCmd cmdGenerateGarbage = new CreateGarbageCmd(OPTION_GARBAGE_NODES_COUNT, "4", OPTION_GARBAGE_NODES_PARENT_COUNT, "3",
+                OPTION_GARBAGE_TYPE, "1");
+        cmdGenerateGarbage.run();
+        Closer cmd1Closer = cmdGenerateGarbage.getCloser();
+
+        // cleanup garbage
+        CreateGarbageCmd cmdClean = new CreateGarbageCmd("clean");
+        cmdClean.run();
+        closer = cmdClean.getCloser();
+
+        NodeDocument garbageRoot = getDocument(cmdClean, Collection.NODES, Utils.getIdFromPath("/" + CreateGarbageCommand.GARBAGE_GEN_ROOT_PATH), 0);
+        // garbage root node should be either deleted or empty (no children)
+        assertTrue(garbageRoot == null || !garbageRoot.hasChildren());
+
+        try {
+            cmd1Closer.close();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * This test generates garbage gap orphans and then cleans up the garbage.
+     * Long running test, should only be run manually.
+     */
+    @Ignore
+    @Test
+    public void cleanupAllGarbageGapOrphans() {
+
+        // generate garbage first
+        CreateGarbageCmd cmdGenerateGarbage = new CreateGarbageCmd(OPTION_GARBAGE_NODES_COUNT, "10", OPTION_GARBAGE_NODES_PARENT_COUNT, "1",
+                OPTION_GARBAGE_TYPE, "2");
+        cmdGenerateGarbage.run();
+        Closer cmd1Closer = cmdGenerateGarbage.getCloser();
+
+        // cleanup garbage
+        CreateGarbageCmd cmdClean = new CreateGarbageCmd("clean");
+        cmdClean.run();
+        closer = cmdClean.getCloser();
+
+        NodeDocument garbageRoot = getDocument(cmdClean, Collection.NODES, Utils.getIdFromPath("/" + CreateGarbageCommand.GARBAGE_GEN_ROOT_PATH), 0);
+        // garbage root node should be either deleted or empty (no children)
+        assertTrue(garbageRoot == null || !garbageRoot.hasChildren());
+
+        try {
+            cmd1Closer.close();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Test
+    public void generateGarbageEmptyPropsUnderOneParent() {
+        ns.dispose();
+
+        CreateGarbageCmd cmd = new CreateGarbageCmd(OPTION_GARBAGE_NODES_COUNT, "10", OPTION_GARBAGE_NODES_PARENT_COUNT, "1",
+                OPTION_GARBAGE_TYPE, "1");
+        String output = captureSystemOut(cmd);
+        closer = cmd.getCloser();
+
+        List<String> generatedBasePaths = cmd.getGeneratedBasePaths();
+
+        DocumentNodeStore nodeStore = cmd.getCommand().getDocumentNodeStore();
+        NodeState garbageRootNodeState = getGarbageRootNode(nodeStore);
+        List<String> garbageRootNodeChildNames = getChildNodeNames(garbageRootNodeState);
+
+        assertEquals(garbageRootNodeChildNames.size(), 1);
+        for (String generateBasePath : generatedBasePaths) {
+            assertTrue(garbageRootNodeChildNames.contains(generateBasePath));
+        }
+
+        List<String> propParentsGarbageNodeNames = getChildNodeNames(garbageRootNodeState.getChildNode(garbageRootNodeChildNames.get(0)));
+        assertEquals(propParentsGarbageNodeNames.size(), 1);
+
+        NodeState emptyPropsParentNode = garbageRootNodeState.getChildNode(garbageRootNodeChildNames.get(0)).getChildNode(propParentsGarbageNodeNames.get(0));
+        List<String> propGarbageNodeNames = getChildNodeNames(emptyPropsParentNode);
+        assertEquals(propGarbageNodeNames.size(), 10);
+
+        for (String propGarbageNodeName : propGarbageNodeNames) {
+            NodeState propGarbageNode = emptyPropsParentNode.getChildNode(propGarbageNodeName);
+            assertNull(propGarbageNode.getProperty(EMPTY_PROPERTY_NAME));
+        }
+    }
+
+    @Test
+    public void generateGarbageGapOrphansUnderOneParent() {
+        ns.dispose();
+
+        int garbageNodesCount = 10;
+        int garbageNodesParentCount = 1;
+
+        testGenerateGapOrphans(garbageNodesCount, garbageNodesParentCount);
+    }
+
+    @Test
+    public void generateGarbageGapOrphansUnderMultipleParents() {
+        ns.dispose();
+
+        int garbageNodesCount = 20;
+        int garbageNodesParentCount = 4;
+
+        testGenerateGapOrphans(garbageNodesCount, garbageNodesParentCount);
+    }
+
+    @Test
+    public void generateGarbageGapOrphansWithDepthAndMinLevelGap() {
+        ns.dispose();
+
+        int garbageNodesCount = 5;
+        int garbageNodesParentCount = 1;
+        int orphansDepth = 7;
+        int orphansLevelGap = 0;
+
+        testGenerateGapOrphans(garbageNodesCount, garbageNodesParentCount, orphansDepth, orphansLevelGap);
+    }
+
+    @Test
+    public void generateGarbageGapOrphansWithDepthAndMaxLevelMinusOneGap() {
+        ns.dispose();
+
+        int garbageNodesCount = 1;
+        int garbageNodesParentCount = 1;
+        int orphansDepth = 10;
+        int orphansLevelGap = 9;
+
+        testGenerateGapOrphans(garbageNodesCount, garbageNodesParentCount, orphansDepth, orphansLevelGap);
+    }
+
+    @Test
+    public void generateGarbageGapOrphansWithDepthAndMaxLevelGap() {
+        ns.dispose();
+
+        int garbageNodesCount = 5;
+        int garbageNodesParentCount = 1;
+        int orphansDepth = 11;
+        int orphansLevelGap = 11;
+
+        testGenerateGapOrphans(garbageNodesCount, garbageNodesParentCount, orphansDepth, orphansLevelGap);
+    }
+
+    @Test
+    public void generateGarbageGapOrphansWithDepthAndMiddleLevelGap() {
+        ns.dispose();
+
+        int garbageNodesCount = 5;
+        int garbageNodesParentCount = 1;
+        int orphansDepth = 8;
+        int orphansLevelGap = 4;
+
+        testGenerateGapOrphans(garbageNodesCount, garbageNodesParentCount, orphansDepth, orphansLevelGap);
+    }
+
+    /**
+     * Calls the GenerateFullGCCommand to generate gap orphans and verifies the generated gap orphans were created
+     * correctly.
+     * Gap orphan depth is minimum and level gap is 0.
+     * @param garbageNodesCount
+     * @param garbageNodesParentCount
+     */
+    private void testGenerateGapOrphans(int garbageNodesCount, int garbageNodesParentCount) {
+        testGenerateGapOrphans(garbageNodesCount, garbageNodesParentCount, 1, 0);
+    }
+
+    /**
+     * Calls the GenerateFullGCCommand to generate gap orphans and verifies the generated gap orphans:
+     * - checks that parents of gap orphans are missing by retrieving the nodes from the document store
+     * - checks that the expected number gap orphans are generated by retrieving the nodes from the document store
+     * @param garbageNodesCount
+     * @param garbageNodesParentCount
+     * @param orphansDepth - the depth of the leaf garbage gap orphan node
+     * @param orphansLevelGap - the level that is used as threshold for deletion - all nodes from the parent to this node
+     *                        will be deleted
+     */
+    private void testGenerateGapOrphans(int garbageNodesCount, int garbageNodesParentCount,
+                                        int orphansDepth, int orphansLevelGap) {
+        CreateGarbageCmd cmd = new CreateGarbageCmd(OPTION_GARBAGE_NODES_COUNT, String.valueOf(garbageNodesCount),
+                OPTION_GARBAGE_NODES_PARENT_COUNT, String.valueOf(garbageNodesParentCount),
+                OPTION_GARBAGE_ORPHANS_DEPTH, String.valueOf(orphansDepth),
+                OPTION_GARBAGE_ORPHANS_LEVEL_GAP, String.valueOf(orphansLevelGap),
+                OPTION_GARBAGE_TYPE, "2");
+        String output = captureSystemOut(cmd);
+        closer = cmd.getCloser();
+
+        int nodesPerParent = garbageNodesCount / garbageNodesParentCount;
+
+        List<String> generatedBasePaths = cmd.getGeneratedBasePaths();
+
+        DocumentNodeStore nodeStore = cmd.getCommand().getDocumentNodeStore();
+        NodeState garbageRootNodeState = getGarbageRootNode(nodeStore);
+        List<String> garbageRootNodeChildNames = getChildNodeNames(garbageRootNodeState);
+
+        assertEquals(garbageRootNodeChildNames.size(), 1);
+        for (String generateBasePath : generatedBasePaths) {
+            assertTrue(garbageRootNodeChildNames.contains(generateBasePath));
+        }
+
+        String generateBasePath = generatedBasePaths.get(0);
+        for (int i = 0; i < garbageNodesParentCount; i ++) {
+
+            for (int j = 0; j < nodesPerParent; j++) {
+
+                // check deleted node paths
+                List<String> deletedNodePaths = CreateGarbageCommand.generateGapOrphanNodePaths(
+                        generateBasePath, i, j, orphansDepth, orphansLevelGap, false);
+                for (String deletedNodePath : deletedNodePaths) {
+                    NodeDocument missingDocument = getDocument(cmd, Collection.NODES, deletedNodePath, 0);
+                    assertNull(missingDocument);
+                }
+
+                // check not deleted node paths - remaining gap orphans
+                List<String> notDeletedNodePaths = CreateGarbageCommand.generateGapOrphanNodePaths(generateBasePath, i, j, orphansDepth, orphansLevelGap, true);
+                for (String notDeletedNodePath : notDeletedNodePaths) {
+                    NodeDocument gapOrphanDocument = getDocument(cmd, Collection.NODES, notDeletedNodePath, 0);
+                    assertNotNull(gapOrphanDocument);
+                }
+            }
+        }
+    }
+
+    /**
+     * Utility method that returns the root node of the full gc generated garbage nodes.
+     * @param nodeStore
+     * @return
+     */
+    private static @NotNull NodeState getGarbageRootNode(DocumentNodeStore nodeStore) {
+        NodeState garbageRootNodeState = nodeStore.getRoot().getChildNode(CreateGarbageCommand.GARBAGE_GEN_ROOT_PATH_BASE).
+                getChildNode(CreateGarbageCommand.GARBAGE_GEN_ROOT_NODE_NAME);
+        return garbageRootNodeState;
+    }
+
+    /**
+     * Used in unit tests for retrieving a document by path from the document store using the documentNodeStore
+     * of this class.
+     * @param collection
+     * @param key
+     * @param maxCacheAge
+     * @return
+     * @param <T>
+     * @throws DocumentStoreException
+     */
+    @Nullable
+    private  <T extends Document> T getDocument(CreateGarbageCmd cmd, Collection<T> collection, String key, int maxCacheAge)
+            throws DocumentStoreException {
+        return cmd.getCommand().getDocumentNodeStore().getDocumentStore().find(collection, key, maxCacheAge);
+    }
+
+    /**
+     * Wrapper class for the GenerateGarbageCommand to be used in unit tests.
+     */
+    private static class CreateGarbageCmd implements Runnable {
+
+        private final ImmutableList<String> args;
+        private CreateGarbageCommand command;
+        private Closer closer;
+        private List<String> generatedBasePaths;
+
+        public CreateGarbageCmd(String... args) {
+            // append the default mongodb connection string if one was not provided
+            if (args[0].startsWith("mongodb://")) {
+                this.args = ImmutableList.copyOf(args);

Review Comment:
   please avoid the usage of guava collections. We have this OAK-10685 epic to remove guava from Oak.
   
   You can use `List.copyOf` with only the caveat that it doesn't allow `null` values.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "rishabhdaim (via GitHub)" <gi...@apache.org>.
rishabhdaim commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1831385389


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/plugins/document/GenerateGarbageHelper.java:
##########
@@ -0,0 +1,37 @@
+/*
+ * 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;
+
+/**
+ * Helper class to generate garbage for testing purposes.
+ */
+public class GenerateGarbageHelper {

Review Comment:
   ```suggestion
   public class CreateGarbageHelper {
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "shodaaan (via GitHub)" <gi...@apache.org>.
shodaaan commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1812633888


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/plugins/document/GenerateGarbageHelper.java:
##########
@@ -0,0 +1,19 @@
+package org.apache.jackrabbit.oak.plugins.document;
+
+/**
+ * Helper class to generate garbage for testing purposes.
+ */
+public class GenerateGarbageHelper {
+
+    public static boolean isInvalidGarbageGenerationMode(int fullGCMode) {
+        return fullGCMode == 0;
+    }
+
+    public static boolean includesEmptyProps(int fullGCMode) {

Review Comment:
   Fixed, thank you for the suggestion.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "shodaaan (via GitHub)" <gi...@apache.org>.
shodaaan commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1820824120


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateGarbageCommand.java:
##########
@@ -0,0 +1,553 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.commons.concurrent.ExecutorCloser;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.GenerateGarbageHelper;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateGarbageCommand implements Command, Closeable {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateGarbageCommand.class);
+
+    private final ScheduledExecutorService continuousRunExecutor = Executors.newScheduledThreadPool(1);
+
+    private static final String USAGE = "createGarbage {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * FullGC garbage should be generated under tmp node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH_BASE = "tmp";
+
+    /**
+     * The root node name for fullGC garbage generation, one level under tmp.
+     */
+    public static String GARBAGE_GEN_ROOT_NODE_NAME = "fullGC_GarbageRoot";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH = GARBAGE_GEN_ROOT_PATH_BASE + "/" + GARBAGE_GEN_ROOT_NODE_NAME;
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String GEN_BASE_PATH = "GenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String GEN_PARENT_NODE_PREFIX = "Parent_";
+    public static String GEN_NODE_PREFIX = "Node_";
+    public static String GEN_NODE_LEVEL_PREFIX = "_Level_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    /**
+     * The maximum depth in the tree at which to generate gap orphans garbage nodes.
+     * Used for validation of the orphansDepth / orphansLevelGap options.
+     */
+    private final int GAP_ORPHANS_MAX_DEPTH = 15;
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node FULLGC_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> orphansDepth;
+        final OptionSpec<Integer> orphansLevelGap;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public GenerateFullGCOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            orphansDepth = parser
+                    .accepts("orphansDepth", "the depth in the tree at which to create garbage nodes. Only applies to ORPHANS fullGC modes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            orphansLevelGap = parser
+                    .accepts("orphansLevelGap", "the gap in the tree between the first leaf gap orphan garbage node and its' parent. Only applies to ORPHANS fullGC modes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public GenerateFullGCOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getOrphansDepth() { return orphansDepth.value(options);}
+
+        public int getOrphansLevelGap() { return orphansLevelGap.value(options); }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {
+
+        try (Closer closer = Closer.create()) {
+            // Register the ExecutorService with Closer
+            closer.register(() -> {
+                // Properly shutdown the executor service
+                continuousRunExecutor.shutdown();
+                try {
+                    if (!continuousRunExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
+                        continuousRunExecutor.shutdownNow();
+                    }
+                } catch (InterruptedException e) {
+                    throw new RuntimeException(e);
+                }
+            });
+            execute(closer, args);
+        } catch (Throwable e) {
+            LOG.error("Command failed", e);
+            throw e;
+        }
+    }
+
+    /**
+     * Method with passed closer is necessary in order to allow for unit tests to check the output of the command.
+     * It is the responsibility of the caller to close the closer.
+     *
+     * Returns the list of generated garbage base paths (under the garbage root node).
+     * @param closer
+     * @param args
+     * @throws Exception
+     */
+    public List<String> execute(Closer closer, String... args) throws Exception {
+        continuousRunIndex = 0;
+
+        List<String> generateBasePaths = new ArrayList<>();
+
+        GenerateFullGCOptions options = new GenerateFullGCOptions(USAGE).parse(args);
+        String subCmd = options.getSubCmd();
+
+        if (GenerateFullGCOptions.CMD_GENERATE.equals(subCmd)) {
+
+            if (options.getCreateGarbageNodesCount() <= 0) {
+                LOG.error("Invalid garbageNodesCount specified: " + options.getCreateGarbageNodesCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+            if (options.getGarbageNodesParentCount() <= 0) {
+                LOG.error("Invalid garbageNodesParentCount specified: " + options.getGarbageNodesParentCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+
+            if (options.getNumberOfRuns() > 1 && options.getGenerateIntervalSeconds() > 0) {
+                generateBasePaths.addAll(generateGarbageContinuously(options, closer));
+            } else {
+                generateBasePaths.add(generateGarbage(options, closer, 0));
+            }
+        } else if (GenerateFullGCOptions.CMD_CLEAN.equals(subCmd)) {
+            cleanGarbage(options, closer);
+        } else {
+            System.err.println("Unknown revisions command: " + subCmd);
+        }
+
+        return generateBasePaths;
+    }
+
+    @Override
+    public void close() throws IOException {
+        new ExecutorCloser(this.continuousRunExecutor).close();
+    }
+
+    private List<String> generateGarbageContinuously(GenerateFullGCOptions options, Closer closer) throws Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long startGenTimestamp = System.currentTimeMillis();
+
+        List<String> generatedGarbageBasePaths = new ArrayList<>();
+
+        int numberOfRuns = options.getNumberOfRuns();
+        int intervalSeconds = options.getGenerateIntervalSeconds();
+        Runnable task = () -> {
+            if (continuousRunIndex < numberOfRuns) {
+                try {
+                    String genBasePath = generateGarbage(options, continuousRunIndex, builder, startGenTimestamp);
+                    generatedGarbageBasePaths.add(genBasePath);
+                } catch (Exception e) {
+                    LOG.error("Error generating garbage in run " + continuousRunIndex, e);
+                }
+                LOG.info("Task executed. Count: " + (continuousRunIndex + 1));
+                continuousRunIndex++;
+            } else {
+                // Shutdown the executor once the task has run numberOfRuns times
+                continuousRunExecutor.shutdown();
+                LOG.info("Task completed " + numberOfRuns + " times. Stopping execution.");
+            }
+        };
+
+        // Schedule the task to run every intervalSeconds
+        continuousRunExecutor.scheduleAtFixedRate(task, 0, intervalSeconds, TimeUnit.SECONDS);
+
+        return generatedGarbageBasePaths;
+    }
+
+    /**
+     * Generate garbage nodes in the repository in order to allow for testing fullGC functionality.
+     *
+     * Returns the path of the generated FULLGC_GEN_BASE_PATH node (under the root).
+     * @param options
+     * @param closer
+     * @param runIndex
+     * @return
+     * @throws IOException
+     * @throws Exception
+     */
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long generationTimestamp = System.currentTimeMillis();
+
+        return generateGarbage(options, runIndex, builder, generationTimestamp);
+    }
+
+    private String generateGarbage(GenerateFullGCOptions options, int runIndex,
+                                   DocumentNodeStoreBuilder<?> builder, long timestamp) throws Exception {
+
+        if (builder == null) {
+            System.err.println("generateFullGC mode only available for DocumentNodeStore");
+            System.exit(1);
+        }
+
+        if (GenerateGarbageHelper.isInvalidGarbageGenerationMode(options.getGarbageType())) {
+            LOG.error("Invalid fullGCMode specified: " + options.getGarbageType() + " in: " + getClass().getName());
+            System.exit(1);
+        }
+
+        String generationBasePath = GEN_BASE_PATH + timestamp + "_" + runIndex;
+        documentNodeStore = builder.build();
+
+        NodeBuilder rootNode = documentNodeStore.getRoot().builder();
+
+        if (GenerateGarbageHelper.isEmptyProps(options.getGarbageType())) {
+            generateGarbageEmptyProps(rootNode, options, generationBasePath);
+        }
+        else if (GenerateGarbageHelper.isGapOrphans(options.getGarbageType())) {
+            generateGarbageGapOrphans(rootNode, options, generationBasePath);
+        }
+
+        return generationBasePath;
+    }
+    private void generateGarbageGapOrphans(NodeBuilder rootNode, GenerateFullGCOptions options, String generationBasePath)
+            throws CommitFailedException {
+
+        // validate gap orphans depth and level gap
+        if (options.getOrphansDepth() < 0 || options.getOrphansDepth() > GAP_ORPHANS_MAX_DEPTH) {
+            LOG.error("Invalid orphansDepth specified: " + options.getOrphansDepth() + " in: " + getClass().getName()
+                    + ". Must be an integer >= 0 and <= " + GAP_ORPHANS_MAX_DEPTH);
+            System.exit(1);
+        }
+        if (options.getOrphansLevelGap() < 0 || options.getOrphansLevelGap() > options.getOrphansDepth()) {
+            LOG.error("Invalid orphansLevelGap specified: " + options.getOrphansLevelGap() + " in: " + getClass().getName()
+                    + ". Must be a positive integer <= orphansDepth");
+            System.exit(1);
+        }
+
+        System.out.println("Generating fullGC gap orphans on the document: " + generationBasePath);
+
+        NodeBuilder garbageRootNode = rootNode.child(GARBAGE_GEN_ROOT_PATH_BASE).child(GARBAGE_GEN_ROOT_NODE_NAME);
+        garbageRootNode.child(generationBasePath).setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+
+        int nodesCountUnderParent = options.getCreateGarbageNodesCount() / options.getGarbageNodesParentCount();
+        for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+            // create parent node
+            garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).setProperty(JcrConstants.JCR_PRIMARYTYPE, "nt:folder", NAME);
+
+            // create child nodes under parent, according to gap orphans depth
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                getGapOrphanLeafGarbageNode(garbageRootNode, generationBasePath, options.getOrphansDepth(), i, j).
+                        setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+            }
+        }
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+
+        // Generate garbage nodes - GAP_ORPHANS - remove parent nodes
+        StringBuilder sbNodePath = new StringBuilder();
+        List<String> deleteNodePaths = new ArrayList<>();
+        for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+
+            // append the parent to the paths to delete
+            sbNodePath.setLength(0);
+            String path = getIdFromPath(
+                    sbNodePath.append("/").append(GARBAGE_GEN_ROOT_PATH).append("/").append(generationBasePath).append("/")
+                            .append(GEN_PARENT_NODE_PREFIX).append(i).toString());
+            deleteNodePaths.add(path);
+
+            // append all the gap orphans nodes between the parent and the level gap to the paths to delete
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                deleteNodePaths.addAll(generateGapOrphanNodePaths(generationBasePath, i, j, options.getOrphansDepth(), options.getOrphansLevelGap(), false));
+            }
+        }
+        // Remove all parent nodes
+        documentNodeStore.getDocumentStore().remove(org.apache.jackrabbit.oak.plugins.document.Collection.NODES, deleteNodePaths);
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+    }
+
+    private NodeBuilder getGapOrphanLeafGarbageNode(NodeBuilder garbageRootNode, String generationBasePath, int depth, int parentIndex, int nodeIndex) {
+        NodeBuilder garbageNode = garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + parentIndex);
+        for (int i = 1; i < depth; i++) {
+            garbageNode = garbageNode.child(GEN_NODE_PREFIX + nodeIndex + GEN_NODE_LEVEL_PREFIX + i);
+        }
+        // the leaf garbage node does not have level in its name
+        garbageNode = garbageNode.child(GEN_NODE_PREFIX + nodeIndex);
+        return garbageNode;
+    }
+
+    public static List<String> generateGapOrphanNodePaths(String generationBasePath, int parentIndex, int nodeIndex, int gapDepth) {
+        return generateGapOrphanNodePaths(generationBasePath, parentIndex, nodeIndex, gapDepth, 0, false);
+    }
+
+    /**
+     * Gets the paths of the nodes between the parent node and the level gap OR between the level gap and the leaf
+     * gap orphan node.
+     *
+     * Example 1: gapDepth = 4, gapLevelGap = 2, fromLevelGapToLeaf = false will return:
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2
+     * and will NOT return the rest of the nodes:
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3/Node_0 (the leaf node)
+     *
+     * Example 2: gapDepth = 5, gapLevelGap = 2, fromLevelGapToLeaf = true will return:
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3/Node_0_Level_4
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3/Node_0_Level_4/Node_0 (the leaf node)
+     * and will NOT return the nodes from the parent to the gap level:
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2
+     *
+     * @param generationBasePath
+     * @param parentIndex
+     * @param nodeIndex
+     * @param gapDepth - the depth in the tree until which to create gap orphan garbage nodes.
+     * @param gapLevelGap - the gap in the tree between the first gap orphan garbage node and its parent.
+     * @param fromLevelGapToLeaf - if true, the paths will be generated from the level gap to the leaf node, otherwise the paths will be generated from the parent node to the level gap.
+     * @return
+     */
+    public static List<String> generateGapOrphanNodePaths(String generationBasePath, int parentIndex, int nodeIndex, int gapDepth, int gapLevelGap, boolean fromLevelGapToLeaf) {
+        List<String> gapOrphanNodePaths = new ArrayList<>();
+
+        StringBuilder sbNodePath = new StringBuilder();
+        sbNodePath.setLength(0);
+
+        // start path from the parent node
+        sbNodePath.append("/").append(GARBAGE_GEN_ROOT_PATH).append("/").append(generationBasePath).append("/")
+                .append(GEN_PARENT_NODE_PREFIX).append(parentIndex);
+
+        for(int i = 1; i <= gapDepth; i ++) {
+            sbNodePath.append("/").append(GEN_NODE_PREFIX).append(nodeIndex);
+            // the leaf node does NOT have level in its name
+            if (i < gapDepth) {
+                sbNodePath.append(GEN_NODE_LEVEL_PREFIX).append(i);
+            }
+
+            // only append to list if the criteria for returning the node path is met
+            if ((!fromLevelGapToLeaf && i < gapLevelGap) || (fromLevelGapToLeaf && i >= gapLevelGap)) {
+                gapOrphanNodePaths.add(getIdFromPath(sbNodePath.toString()));
+            }
+        }
+
+        return gapOrphanNodePaths;
+    }
+
+    private void generateGarbageEmptyProps(NodeBuilder rootNode, GenerateFullGCOptions options, String generationBasePath) throws CommitFailedException {
+
+        System.out.println("Generating fullGC empty props on the document: " + generationBasePath);
+
+        NodeBuilder garbageRootNode = rootNode.child(GARBAGE_GEN_ROOT_PATH_BASE).child(GARBAGE_GEN_ROOT_NODE_NAME);
+        garbageRootNode.child(generationBasePath).setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+
+        int nodesCountUnderParent = options.getCreateGarbageNodesCount() / options.getGarbageNodesParentCount();
+        for(int i = 0; i < options.getGarbageNodesParentCount(); i ++) {
+            garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).setProperty(JcrConstants.JCR_PRIMARYTYPE, "nt:folder", NAME);
+
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                        setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+
+                if (GenerateGarbageHelper.isEmptyProps(options.getGarbageType())) {
+                    garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                            setProperty(EMPTY_PROPERTY_NAME, "bar", STRING);
+                }
+            }
+        }
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+
+
+        //2. Generate garbage nodes - EMPTY_PROPERTIES
+        for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+            for (int j = 0; j < nodesCountUnderParent; j++) {
+                garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                        removeProperty(EMPTY_PROPERTY_NAME);
+            }
+        }
+
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();

Review Comment:
   Fixed.



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateGarbageCommand.java:
##########
@@ -0,0 +1,553 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.commons.concurrent.ExecutorCloser;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.GenerateGarbageHelper;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateGarbageCommand implements Command, Closeable {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateGarbageCommand.class);
+
+    private final ScheduledExecutorService continuousRunExecutor = Executors.newScheduledThreadPool(1);
+
+    private static final String USAGE = "createGarbage {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * FullGC garbage should be generated under tmp node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH_BASE = "tmp";
+
+    /**
+     * The root node name for fullGC garbage generation, one level under tmp.
+     */
+    public static String GARBAGE_GEN_ROOT_NODE_NAME = "fullGC_GarbageRoot";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH = GARBAGE_GEN_ROOT_PATH_BASE + "/" + GARBAGE_GEN_ROOT_NODE_NAME;
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String GEN_BASE_PATH = "GenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String GEN_PARENT_NODE_PREFIX = "Parent_";
+    public static String GEN_NODE_PREFIX = "Node_";
+    public static String GEN_NODE_LEVEL_PREFIX = "_Level_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    /**
+     * The maximum depth in the tree at which to generate gap orphans garbage nodes.
+     * Used for validation of the orphansDepth / orphansLevelGap options.
+     */
+    private final int GAP_ORPHANS_MAX_DEPTH = 15;
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node FULLGC_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> orphansDepth;
+        final OptionSpec<Integer> orphansLevelGap;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public GenerateFullGCOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            orphansDepth = parser
+                    .accepts("orphansDepth", "the depth in the tree at which to create garbage nodes. Only applies to ORPHANS fullGC modes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            orphansLevelGap = parser
+                    .accepts("orphansLevelGap", "the gap in the tree between the first leaf gap orphan garbage node and its' parent. Only applies to ORPHANS fullGC modes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public GenerateFullGCOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getOrphansDepth() { return orphansDepth.value(options);}
+
+        public int getOrphansLevelGap() { return orphansLevelGap.value(options); }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {
+
+        try (Closer closer = Closer.create()) {
+            // Register the ExecutorService with Closer
+            closer.register(() -> {
+                // Properly shutdown the executor service
+                continuousRunExecutor.shutdown();
+                try {
+                    if (!continuousRunExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
+                        continuousRunExecutor.shutdownNow();
+                    }
+                } catch (InterruptedException e) {
+                    throw new RuntimeException(e);
+                }
+            });
+            execute(closer, args);
+        } catch (Throwable e) {
+            LOG.error("Command failed", e);
+            throw e;
+        }
+    }
+
+    /**
+     * Method with passed closer is necessary in order to allow for unit tests to check the output of the command.
+     * It is the responsibility of the caller to close the closer.
+     *
+     * Returns the list of generated garbage base paths (under the garbage root node).
+     * @param closer
+     * @param args
+     * @throws Exception
+     */
+    public List<String> execute(Closer closer, String... args) throws Exception {
+        continuousRunIndex = 0;
+
+        List<String> generateBasePaths = new ArrayList<>();
+
+        GenerateFullGCOptions options = new GenerateFullGCOptions(USAGE).parse(args);
+        String subCmd = options.getSubCmd();
+
+        if (GenerateFullGCOptions.CMD_GENERATE.equals(subCmd)) {
+
+            if (options.getCreateGarbageNodesCount() <= 0) {
+                LOG.error("Invalid garbageNodesCount specified: " + options.getCreateGarbageNodesCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+            if (options.getGarbageNodesParentCount() <= 0) {
+                LOG.error("Invalid garbageNodesParentCount specified: " + options.getGarbageNodesParentCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+
+            if (options.getNumberOfRuns() > 1 && options.getGenerateIntervalSeconds() > 0) {
+                generateBasePaths.addAll(generateGarbageContinuously(options, closer));
+            } else {
+                generateBasePaths.add(generateGarbage(options, closer, 0));
+            }
+        } else if (GenerateFullGCOptions.CMD_CLEAN.equals(subCmd)) {
+            cleanGarbage(options, closer);
+        } else {
+            System.err.println("Unknown revisions command: " + subCmd);
+        }
+
+        return generateBasePaths;
+    }
+
+    @Override
+    public void close() throws IOException {
+        new ExecutorCloser(this.continuousRunExecutor).close();
+    }
+
+    private List<String> generateGarbageContinuously(GenerateFullGCOptions options, Closer closer) throws Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long startGenTimestamp = System.currentTimeMillis();
+
+        List<String> generatedGarbageBasePaths = new ArrayList<>();
+
+        int numberOfRuns = options.getNumberOfRuns();
+        int intervalSeconds = options.getGenerateIntervalSeconds();
+        Runnable task = () -> {
+            if (continuousRunIndex < numberOfRuns) {
+                try {
+                    String genBasePath = generateGarbage(options, continuousRunIndex, builder, startGenTimestamp);
+                    generatedGarbageBasePaths.add(genBasePath);
+                } catch (Exception e) {
+                    LOG.error("Error generating garbage in run " + continuousRunIndex, e);
+                }
+                LOG.info("Task executed. Count: " + (continuousRunIndex + 1));
+                continuousRunIndex++;
+            } else {
+                // Shutdown the executor once the task has run numberOfRuns times
+                continuousRunExecutor.shutdown();
+                LOG.info("Task completed " + numberOfRuns + " times. Stopping execution.");
+            }
+        };
+
+        // Schedule the task to run every intervalSeconds
+        continuousRunExecutor.scheduleAtFixedRate(task, 0, intervalSeconds, TimeUnit.SECONDS);
+
+        return generatedGarbageBasePaths;
+    }
+
+    /**
+     * Generate garbage nodes in the repository in order to allow for testing fullGC functionality.
+     *
+     * Returns the path of the generated FULLGC_GEN_BASE_PATH node (under the root).
+     * @param options
+     * @param closer
+     * @param runIndex
+     * @return
+     * @throws IOException
+     * @throws Exception
+     */
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long generationTimestamp = System.currentTimeMillis();
+
+        return generateGarbage(options, runIndex, builder, generationTimestamp);
+    }
+
+    private String generateGarbage(GenerateFullGCOptions options, int runIndex,
+                                   DocumentNodeStoreBuilder<?> builder, long timestamp) throws Exception {
+
+        if (builder == null) {
+            System.err.println("generateFullGC mode only available for DocumentNodeStore");
+            System.exit(1);
+        }
+
+        if (GenerateGarbageHelper.isInvalidGarbageGenerationMode(options.getGarbageType())) {
+            LOG.error("Invalid fullGCMode specified: " + options.getGarbageType() + " in: " + getClass().getName());
+            System.exit(1);
+        }
+
+        String generationBasePath = GEN_BASE_PATH + timestamp + "_" + runIndex;
+        documentNodeStore = builder.build();
+
+        NodeBuilder rootNode = documentNodeStore.getRoot().builder();
+
+        if (GenerateGarbageHelper.isEmptyProps(options.getGarbageType())) {
+            generateGarbageEmptyProps(rootNode, options, generationBasePath);
+        }
+        else if (GenerateGarbageHelper.isGapOrphans(options.getGarbageType())) {
+            generateGarbageGapOrphans(rootNode, options, generationBasePath);
+        }
+
+        return generationBasePath;
+    }
+    private void generateGarbageGapOrphans(NodeBuilder rootNode, GenerateFullGCOptions options, String generationBasePath)
+            throws CommitFailedException {
+
+        // validate gap orphans depth and level gap
+        if (options.getOrphansDepth() < 0 || options.getOrphansDepth() > GAP_ORPHANS_MAX_DEPTH) {
+            LOG.error("Invalid orphansDepth specified: " + options.getOrphansDepth() + " in: " + getClass().getName()
+                    + ". Must be an integer >= 0 and <= " + GAP_ORPHANS_MAX_DEPTH);
+            System.exit(1);
+        }
+        if (options.getOrphansLevelGap() < 0 || options.getOrphansLevelGap() > options.getOrphansDepth()) {
+            LOG.error("Invalid orphansLevelGap specified: " + options.getOrphansLevelGap() + " in: " + getClass().getName()
+                    + ". Must be a positive integer <= orphansDepth");
+            System.exit(1);
+        }
+
+        System.out.println("Generating fullGC gap orphans on the document: " + generationBasePath);
+
+        NodeBuilder garbageRootNode = rootNode.child(GARBAGE_GEN_ROOT_PATH_BASE).child(GARBAGE_GEN_ROOT_NODE_NAME);
+        garbageRootNode.child(generationBasePath).setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+
+        int nodesCountUnderParent = options.getCreateGarbageNodesCount() / options.getGarbageNodesParentCount();
+        for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+            // create parent node
+            garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).setProperty(JcrConstants.JCR_PRIMARYTYPE, "nt:folder", NAME);
+
+            // create child nodes under parent, according to gap orphans depth
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                getGapOrphanLeafGarbageNode(garbageRootNode, generationBasePath, options.getOrphansDepth(), i, j).
+                        setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+            }
+        }
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+
+        // Generate garbage nodes - GAP_ORPHANS - remove parent nodes
+        StringBuilder sbNodePath = new StringBuilder();
+        List<String> deleteNodePaths = new ArrayList<>();
+        for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+
+            // append the parent to the paths to delete
+            sbNodePath.setLength(0);
+            String path = getIdFromPath(
+                    sbNodePath.append("/").append(GARBAGE_GEN_ROOT_PATH).append("/").append(generationBasePath).append("/")
+                            .append(GEN_PARENT_NODE_PREFIX).append(i).toString());
+            deleteNodePaths.add(path);
+
+            // append all the gap orphans nodes between the parent and the level gap to the paths to delete
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                deleteNodePaths.addAll(generateGapOrphanNodePaths(generationBasePath, i, j, options.getOrphansDepth(), options.getOrphansLevelGap(), false));
+            }
+        }
+        // Remove all parent nodes
+        documentNodeStore.getDocumentStore().remove(org.apache.jackrabbit.oak.plugins.document.Collection.NODES, deleteNodePaths);
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+    }
+
+    private NodeBuilder getGapOrphanLeafGarbageNode(NodeBuilder garbageRootNode, String generationBasePath, int depth, int parentIndex, int nodeIndex) {
+        NodeBuilder garbageNode = garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + parentIndex);
+        for (int i = 1; i < depth; i++) {
+            garbageNode = garbageNode.child(GEN_NODE_PREFIX + nodeIndex + GEN_NODE_LEVEL_PREFIX + i);
+        }
+        // the leaf garbage node does not have level in its name
+        garbageNode = garbageNode.child(GEN_NODE_PREFIX + nodeIndex);
+        return garbageNode;
+    }
+
+    public static List<String> generateGapOrphanNodePaths(String generationBasePath, int parentIndex, int nodeIndex, int gapDepth) {
+        return generateGapOrphanNodePaths(generationBasePath, parentIndex, nodeIndex, gapDepth, 0, false);
+    }
+
+    /**
+     * Gets the paths of the nodes between the parent node and the level gap OR between the level gap and the leaf
+     * gap orphan node.
+     *
+     * Example 1: gapDepth = 4, gapLevelGap = 2, fromLevelGapToLeaf = false will return:
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2
+     * and will NOT return the rest of the nodes:
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3/Node_0 (the leaf node)
+     *
+     * Example 2: gapDepth = 5, gapLevelGap = 2, fromLevelGapToLeaf = true will return:
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3/Node_0_Level_4
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3/Node_0_Level_4/Node_0 (the leaf node)
+     * and will NOT return the nodes from the parent to the gap level:
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1
+     * - /tmp/fullGC_GarbageRoot/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2
+     *
+     * @param generationBasePath
+     * @param parentIndex
+     * @param nodeIndex
+     * @param gapDepth - the depth in the tree until which to create gap orphan garbage nodes.
+     * @param gapLevelGap - the gap in the tree between the first gap orphan garbage node and its parent.
+     * @param fromLevelGapToLeaf - if true, the paths will be generated from the level gap to the leaf node, otherwise the paths will be generated from the parent node to the level gap.
+     * @return
+     */
+    public static List<String> generateGapOrphanNodePaths(String generationBasePath, int parentIndex, int nodeIndex, int gapDepth, int gapLevelGap, boolean fromLevelGapToLeaf) {
+        List<String> gapOrphanNodePaths = new ArrayList<>();
+
+        StringBuilder sbNodePath = new StringBuilder();
+        sbNodePath.setLength(0);
+
+        // start path from the parent node
+        sbNodePath.append("/").append(GARBAGE_GEN_ROOT_PATH).append("/").append(generationBasePath).append("/")
+                .append(GEN_PARENT_NODE_PREFIX).append(parentIndex);
+
+        for(int i = 1; i <= gapDepth; i ++) {
+            sbNodePath.append("/").append(GEN_NODE_PREFIX).append(nodeIndex);
+            // the leaf node does NOT have level in its name
+            if (i < gapDepth) {
+                sbNodePath.append(GEN_NODE_LEVEL_PREFIX).append(i);
+            }
+
+            // only append to list if the criteria for returning the node path is met
+            if ((!fromLevelGapToLeaf && i < gapLevelGap) || (fromLevelGapToLeaf && i >= gapLevelGap)) {
+                gapOrphanNodePaths.add(getIdFromPath(sbNodePath.toString()));
+            }
+        }
+
+        return gapOrphanNodePaths;
+    }
+
+    private void generateGarbageEmptyProps(NodeBuilder rootNode, GenerateFullGCOptions options, String generationBasePath) throws CommitFailedException {
+
+        System.out.println("Generating fullGC empty props on the document: " + generationBasePath);
+
+        NodeBuilder garbageRootNode = rootNode.child(GARBAGE_GEN_ROOT_PATH_BASE).child(GARBAGE_GEN_ROOT_NODE_NAME);
+        garbageRootNode.child(generationBasePath).setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+
+        int nodesCountUnderParent = options.getCreateGarbageNodesCount() / options.getGarbageNodesParentCount();
+        for(int i = 0; i < options.getGarbageNodesParentCount(); i ++) {
+            garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).setProperty(JcrConstants.JCR_PRIMARYTYPE, "nt:folder", NAME);
+
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                        setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+
+                if (GenerateGarbageHelper.isEmptyProps(options.getGarbageType())) {
+                    garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                            setProperty(EMPTY_PROPERTY_NAME, "bar", STRING);
+                }
+            }
+        }
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+
+
+        //2. Generate garbage nodes - EMPTY_PROPERTIES
+        for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+            for (int j = 0; j < nodesCountUnderParent; j++) {
+                garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                        removeProperty(EMPTY_PROPERTY_NAME);
+            }
+        }
+
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+    }
+
+    /**
+     * Cleans up all generated garbage by removing the node FULLGC_GEN_ROOT_PATH and all of
+     * its children (recursively)
+     * @param options
+     * @param closer
+     * @throws IOException
+     * @throws Exception
+     */
+    private void cleanGarbage(GenerateFullGCOptions options, Closer closer) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+
+        if (builder == null) {
+            System.err.println("generateFullGC mode only available for DocumentNodeStore");
+            System.exit(1);
+        }
+
+        System.out.println("Cleaning up all generated garbage:");
+        documentNodeStore = builder.build();
+
+        NodeBuilder rootBuilder = documentNodeStore.getRoot().builder();
+
+        NodeBuilder generatedGarbageRootBuilder = rootBuilder.child(GARBAGE_GEN_ROOT_PATH_BASE).child(GARBAGE_GEN_ROOT_NODE_NAME);
+
+        String garbageRootNodePath = "2:/"+ GARBAGE_GEN_ROOT_PATH;
+        List<String> childNodePaths = new ArrayList<>();
+        childNodePaths.add(garbageRootNodePath);
+
+        // get all paths of the tree nodes under the garbage root node
+        getTreeNodePaths(generatedGarbageRootBuilder, garbageRootNodePath, childNodePaths, 2);
+
+        documentNodeStore.getDocumentStore().remove(org.apache.jackrabbit.oak.plugins.document.Collection.NODES, childNodePaths);
+        documentNodeStore.merge(rootBuilder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();

Review Comment:
   Fixed.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "rishabhdaim (via GitHub)" <gi...@apache.org>.
rishabhdaim commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1831376592


##########
oak-run/src/test/java/org/apache/jackrabbit/oak/plugins/document/CreateGarbageCommandTest.java:
##########
@@ -0,0 +1,437 @@
+/*
+ * 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 org.apache.jackrabbit.guava.common.collect.ImmutableList;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.plugins.document.util.MongoConnection;
+import org.apache.jackrabbit.oak.plugins.document.util.Utils;
+import org.apache.jackrabbit.oak.run.CreateGarbageCommand;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.apache.jackrabbit.oak.plugins.document.CommandTestUtils.captureSystemOut;
+import static org.apache.jackrabbit.oak.run.CreateGarbageCommand.EMPTY_PROPERTY_NAME;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+public class CreateGarbageCommandTest {
+
+    final String OPTION_GARBAGE_NODES_COUNT = "--garbageNodesCount";
+    final String OPTION_GARBAGE_NODES_PARENT_COUNT = "--garbageNodesParentCount";
+    final String OPTION_GARBAGE_TYPE = "--garbageType";
+    final String OPTION_GARBAGE_ORPHANS_DEPTH = "--orphansDepth";
+    final String OPTION_GARBAGE_ORPHANS_LEVEL_GAP = "--orphansLevelGap";
+    final String OPTION_NUMBER_OF_RUNS = "--numberOfRuns";
+    final String OPTION_GENERATE_INTERVAL_SECONDS = "--generateIntervalSeconds";
+
+    @Rule
+    public MongoConnectionFactory connectionFactory = new MongoConnectionFactory();
+
+    @Rule
+    public DocumentMKBuilderProvider builderProvider = new DocumentMKBuilderProvider();
+
+    private DocumentNodeStore ns;
+
+    private Closer closer;
+
+    @BeforeClass
+    public static void assumeMongoDB() {
+        assumeTrue(MongoUtils.isAvailable());
+    }
+
+    @Before
+    public void before() {
+        ns = createDocumentNodeStore();
+    }
+
+    @After
+    public void after() {
+        try {
+            closer.close();
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+        ns.dispose();
+    }
+
+    private DocumentNodeStore createDocumentNodeStore() {
+        MongoConnection c = connectionFactory.getConnection();
+        assertNotNull(c);
+        MongoUtils.dropCollections(c.getDatabase());
+        return builderProvider.newBuilder().setFullGCEnabled(false).setLeaseCheckMode(LeaseCheckMode.DISABLED).setAsyncDelay(0)
+                .setMongoDB(c.getMongoClient(), c.getDBName()).getNodeStore();
+    }
+
+    private List<String> getChildNodeNames(NodeState node) {
+        List<String> childNodeNames = new ArrayList<>();
+        for (String childNodeName : node.getChildNodeNames()) {
+            childNodeNames.add(childNodeName);
+        }
+        return childNodeNames;
+    }
+
+    /**
+     * This test generates garbage empty properties under one parent node and then cleans up the garbage.
+     * Use a custom MongoDB connection string for the MongoDB to run the test on.
+     * Long running test, should only be run manually.
+     */
+    @Ignore
+    @Test
+    public void generateGarbageEmptyPropsUnderOneParentOnCustomMongoDB() {
+        ns.dispose();
+
+        // this should be a valid mongoDB connection string for the MongoDB on which to generate the garbage
+        String mongoDBConnString = "";
+        CreateGarbageCmd cmd = new CreateGarbageCmd( mongoDBConnString, OPTION_GARBAGE_NODES_COUNT, "10", OPTION_GARBAGE_NODES_PARENT_COUNT, "1",
+                OPTION_GARBAGE_TYPE, "1");
+        String output = captureSystemOut(cmd);
+
+        //sleep thread for 10 seconds to allow the garbage generation to complete
+        try {
+            Thread.sleep(10000);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+
+        DocumentNodeStore nodeStore = cmd.getCommand().getDocumentNodeStore();
+        NodeState garbageRootNodeState = getGarbageRootNode(nodeStore);
+        List<String> garbageRootNodeChildNames = getChildNodeNames(garbageRootNodeState);
+
+        assertEquals(garbageRootNodeChildNames.size(), 1);
+        List<String> propParentsGarbageNodeNames = getChildNodeNames(garbageRootNodeState.getChildNode(garbageRootNodeChildNames.get(0)));
+        assertEquals(propParentsGarbageNodeNames.size(), 1);
+
+        List<String> propGarbageNodeNames = getChildNodeNames(garbageRootNodeState.getChildNode(garbageRootNodeChildNames.get(0)).getChildNode(propParentsGarbageNodeNames.get(0)));
+        assertEquals(propGarbageNodeNames.size(), 10);
+    }
+
+    /**
+     * This test generates garbage empty properties and then cleans up the garbage.
+     * Long running test, should only be run manually.
+     */
+    @Ignore
+    @Test
+    public void cleanupAllGarbageEmptyProperties() {
+
+        // generate garbage first
+        CreateGarbageCmd cmdGenerateGarbage = new CreateGarbageCmd(OPTION_GARBAGE_NODES_COUNT, "4", OPTION_GARBAGE_NODES_PARENT_COUNT, "3",
+                OPTION_GARBAGE_TYPE, "1");
+        cmdGenerateGarbage.run();
+        Closer cmd1Closer = cmdGenerateGarbage.getCloser();
+
+        // cleanup garbage
+        CreateGarbageCmd cmdClean = new CreateGarbageCmd("clean");
+        cmdClean.run();
+        closer = cmdClean.getCloser();
+
+        NodeDocument garbageRoot = getDocument(cmdClean, Collection.NODES, Utils.getIdFromPath("/" + CreateGarbageCommand.GARBAGE_GEN_ROOT_PATH), 0);
+        // garbage root node should be either deleted or empty (no children)
+        assertTrue(garbageRoot == null || !garbageRoot.hasChildren());
+
+        try {
+            cmd1Closer.close();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * This test generates garbage gap orphans and then cleans up the garbage.
+     * Long running test, should only be run manually.
+     */
+    @Ignore
+    @Test
+    public void cleanupAllGarbageGapOrphans() {
+
+        // generate garbage first
+        CreateGarbageCmd cmdGenerateGarbage = new CreateGarbageCmd(OPTION_GARBAGE_NODES_COUNT, "10", OPTION_GARBAGE_NODES_PARENT_COUNT, "1",
+                OPTION_GARBAGE_TYPE, "2");
+        cmdGenerateGarbage.run();
+        Closer cmd1Closer = cmdGenerateGarbage.getCloser();
+
+        // cleanup garbage
+        CreateGarbageCmd cmdClean = new CreateGarbageCmd("clean");
+        cmdClean.run();
+        closer = cmdClean.getCloser();
+
+        NodeDocument garbageRoot = getDocument(cmdClean, Collection.NODES, Utils.getIdFromPath("/" + CreateGarbageCommand.GARBAGE_GEN_ROOT_PATH), 0);
+        // garbage root node should be either deleted or empty (no children)
+        assertTrue(garbageRoot == null || !garbageRoot.hasChildren());
+
+        try {
+            cmd1Closer.close();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Test
+    public void generateGarbageEmptyPropsUnderOneParent() {
+        ns.dispose();
+
+        CreateGarbageCmd cmd = new CreateGarbageCmd(OPTION_GARBAGE_NODES_COUNT, "10", OPTION_GARBAGE_NODES_PARENT_COUNT, "1",
+                OPTION_GARBAGE_TYPE, "1");
+        String output = captureSystemOut(cmd);
+        closer = cmd.getCloser();
+
+        List<String> generatedBasePaths = cmd.getGeneratedBasePaths();
+
+        DocumentNodeStore nodeStore = cmd.getCommand().getDocumentNodeStore();
+        NodeState garbageRootNodeState = getGarbageRootNode(nodeStore);
+        List<String> garbageRootNodeChildNames = getChildNodeNames(garbageRootNodeState);
+
+        assertEquals(garbageRootNodeChildNames.size(), 1);
+        for (String generateBasePath : generatedBasePaths) {
+            assertTrue(garbageRootNodeChildNames.contains(generateBasePath));
+        }
+
+        List<String> propParentsGarbageNodeNames = getChildNodeNames(garbageRootNodeState.getChildNode(garbageRootNodeChildNames.get(0)));
+        assertEquals(propParentsGarbageNodeNames.size(), 1);
+
+        NodeState emptyPropsParentNode = garbageRootNodeState.getChildNode(garbageRootNodeChildNames.get(0)).getChildNode(propParentsGarbageNodeNames.get(0));
+        List<String> propGarbageNodeNames = getChildNodeNames(emptyPropsParentNode);
+        assertEquals(propGarbageNodeNames.size(), 10);
+
+        for (String propGarbageNodeName : propGarbageNodeNames) {
+            NodeState propGarbageNode = emptyPropsParentNode.getChildNode(propGarbageNodeName);
+            assertNull(propGarbageNode.getProperty(EMPTY_PROPERTY_NAME));
+        }
+    }
+
+    @Test
+    public void generateGarbageGapOrphansUnderOneParent() {
+        ns.dispose();
+
+        int garbageNodesCount = 10;
+        int garbageNodesParentCount = 1;
+
+        testGenerateGapOrphans(garbageNodesCount, garbageNodesParentCount);
+    }
+
+    @Test
+    public void generateGarbageGapOrphansUnderMultipleParents() {
+        ns.dispose();
+
+        int garbageNodesCount = 20;
+        int garbageNodesParentCount = 4;
+
+        testGenerateGapOrphans(garbageNodesCount, garbageNodesParentCount);
+    }
+
+    @Test
+    public void generateGarbageGapOrphansWithDepthAndMinLevelGap() {
+        ns.dispose();
+
+        int garbageNodesCount = 5;
+        int garbageNodesParentCount = 1;
+        int orphansDepth = 7;
+        int orphansLevelGap = 0;
+
+        testGenerateGapOrphans(garbageNodesCount, garbageNodesParentCount, orphansDepth, orphansLevelGap);
+    }
+
+    @Test
+    public void generateGarbageGapOrphansWithDepthAndMaxLevelMinusOneGap() {
+        ns.dispose();
+
+        int garbageNodesCount = 1;
+        int garbageNodesParentCount = 1;
+        int orphansDepth = 10;
+        int orphansLevelGap = 9;
+
+        testGenerateGapOrphans(garbageNodesCount, garbageNodesParentCount, orphansDepth, orphansLevelGap);
+    }
+
+    @Test
+    public void generateGarbageGapOrphansWithDepthAndMaxLevelGap() {
+        ns.dispose();
+
+        int garbageNodesCount = 5;
+        int garbageNodesParentCount = 1;
+        int orphansDepth = 11;
+        int orphansLevelGap = 11;
+
+        testGenerateGapOrphans(garbageNodesCount, garbageNodesParentCount, orphansDepth, orphansLevelGap);
+    }
+
+    @Test
+    public void generateGarbageGapOrphansWithDepthAndMiddleLevelGap() {
+        ns.dispose();
+
+        int garbageNodesCount = 5;
+        int garbageNodesParentCount = 1;
+        int orphansDepth = 8;
+        int orphansLevelGap = 4;
+
+        testGenerateGapOrphans(garbageNodesCount, garbageNodesParentCount, orphansDepth, orphansLevelGap);
+    }
+
+    /**
+     * Calls the GenerateFullGCCommand to generate gap orphans and verifies the generated gap orphans were created
+     * correctly.
+     * Gap orphan depth is minimum and level gap is 0.
+     * @param garbageNodesCount
+     * @param garbageNodesParentCount
+     */
+    private void testGenerateGapOrphans(int garbageNodesCount, int garbageNodesParentCount) {
+        testGenerateGapOrphans(garbageNodesCount, garbageNodesParentCount, 1, 0);
+    }
+
+    /**
+     * Calls the GenerateFullGCCommand to generate gap orphans and verifies the generated gap orphans:
+     * - checks that parents of gap orphans are missing by retrieving the nodes from the document store
+     * - checks that the expected number gap orphans are generated by retrieving the nodes from the document store
+     * @param garbageNodesCount
+     * @param garbageNodesParentCount
+     * @param orphansDepth - the depth of the leaf garbage gap orphan node
+     * @param orphansLevelGap - the level that is used as threshold for deletion - all nodes from the parent to this node
+     *                        will be deleted
+     */
+    private void testGenerateGapOrphans(int garbageNodesCount, int garbageNodesParentCount,
+                                        int orphansDepth, int orphansLevelGap) {
+        CreateGarbageCmd cmd = new CreateGarbageCmd(OPTION_GARBAGE_NODES_COUNT, String.valueOf(garbageNodesCount),
+                OPTION_GARBAGE_NODES_PARENT_COUNT, String.valueOf(garbageNodesParentCount),
+                OPTION_GARBAGE_ORPHANS_DEPTH, String.valueOf(orphansDepth),
+                OPTION_GARBAGE_ORPHANS_LEVEL_GAP, String.valueOf(orphansLevelGap),
+                OPTION_GARBAGE_TYPE, "2");
+        String output = captureSystemOut(cmd);
+        closer = cmd.getCloser();
+
+        int nodesPerParent = garbageNodesCount / garbageNodesParentCount;
+
+        List<String> generatedBasePaths = cmd.getGeneratedBasePaths();
+
+        DocumentNodeStore nodeStore = cmd.getCommand().getDocumentNodeStore();
+        NodeState garbageRootNodeState = getGarbageRootNode(nodeStore);
+        List<String> garbageRootNodeChildNames = getChildNodeNames(garbageRootNodeState);
+
+        assertEquals(garbageRootNodeChildNames.size(), 1);
+        for (String generateBasePath : generatedBasePaths) {
+            assertTrue(garbageRootNodeChildNames.contains(generateBasePath));
+        }
+
+        String generateBasePath = generatedBasePaths.get(0);
+        for (int i = 0; i < garbageNodesParentCount; i ++) {
+
+            for (int j = 0; j < nodesPerParent; j++) {
+
+                // check deleted node paths
+                List<String> deletedNodePaths = CreateGarbageCommand.generateGapOrphanNodePaths(
+                        generateBasePath, i, j, orphansDepth, orphansLevelGap, false);
+                for (String deletedNodePath : deletedNodePaths) {
+                    NodeDocument missingDocument = getDocument(cmd, Collection.NODES, deletedNodePath, 0);
+                    assertNull(missingDocument);
+                }
+
+                // check not deleted node paths - remaining gap orphans
+                List<String> notDeletedNodePaths = CreateGarbageCommand.generateGapOrphanNodePaths(generateBasePath, i, j, orphansDepth, orphansLevelGap, true);
+                for (String notDeletedNodePath : notDeletedNodePaths) {
+                    NodeDocument gapOrphanDocument = getDocument(cmd, Collection.NODES, notDeletedNodePath, 0);
+                    assertNotNull(gapOrphanDocument);
+                }
+            }
+        }
+    }
+
+    /**
+     * Utility method that returns the root node of the full gc generated garbage nodes.
+     * @param nodeStore
+     * @return
+     */
+    private static @NotNull NodeState getGarbageRootNode(DocumentNodeStore nodeStore) {
+        NodeState garbageRootNodeState = nodeStore.getRoot().getChildNode(CreateGarbageCommand.GARBAGE_GEN_ROOT_PATH_BASE).
+                getChildNode(CreateGarbageCommand.GARBAGE_GEN_ROOT_NODE_NAME);
+        return garbageRootNodeState;
+    }
+
+    /**
+     * Used in unit tests for retrieving a document by path from the document store using the documentNodeStore
+     * of this class.
+     * @param collection
+     * @param key
+     * @param maxCacheAge
+     * @return
+     * @param <T>
+     * @throws DocumentStoreException
+     */
+    @Nullable
+    private  <T extends Document> T getDocument(CreateGarbageCmd cmd, Collection<T> collection, String key, int maxCacheAge)
+            throws DocumentStoreException {
+        return cmd.getCommand().getDocumentNodeStore().getDocumentStore().find(collection, key, maxCacheAge);
+    }
+
+    /**
+     * Wrapper class for the GenerateGarbageCommand to be used in unit tests.
+     */
+    private static class CreateGarbageCmd implements Runnable {
+
+        private final ImmutableList<String> args;
+        private CreateGarbageCommand command;
+        private Closer closer;
+        private List<String> generatedBasePaths;
+
+        public CreateGarbageCmd(String... args) {
+            // append the default mongodb connection string if one was not provided
+            if (args[0].startsWith("mongodb://")) {
+                this.args = ImmutableList.copyOf(args);

Review Comment:
   please avoid the usage of guava collections. We have this OAK-10685 epic to remove guava from Oak.



##########
oak-run/src/test/java/org/apache/jackrabbit/oak/plugins/document/CreateGarbageCommandTest.java:
##########
@@ -0,0 +1,437 @@
+/*
+ * 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 org.apache.jackrabbit.guava.common.collect.ImmutableList;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.plugins.document.util.MongoConnection;
+import org.apache.jackrabbit.oak.plugins.document.util.Utils;
+import org.apache.jackrabbit.oak.run.CreateGarbageCommand;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.apache.jackrabbit.oak.plugins.document.CommandTestUtils.captureSystemOut;
+import static org.apache.jackrabbit.oak.run.CreateGarbageCommand.EMPTY_PROPERTY_NAME;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+public class CreateGarbageCommandTest {
+
+    final String OPTION_GARBAGE_NODES_COUNT = "--garbageNodesCount";
+    final String OPTION_GARBAGE_NODES_PARENT_COUNT = "--garbageNodesParentCount";
+    final String OPTION_GARBAGE_TYPE = "--garbageType";
+    final String OPTION_GARBAGE_ORPHANS_DEPTH = "--orphansDepth";
+    final String OPTION_GARBAGE_ORPHANS_LEVEL_GAP = "--orphansLevelGap";
+    final String OPTION_NUMBER_OF_RUNS = "--numberOfRuns";
+    final String OPTION_GENERATE_INTERVAL_SECONDS = "--generateIntervalSeconds";
+
+    @Rule
+    public MongoConnectionFactory connectionFactory = new MongoConnectionFactory();
+
+    @Rule
+    public DocumentMKBuilderProvider builderProvider = new DocumentMKBuilderProvider();
+
+    private DocumentNodeStore ns;
+
+    private Closer closer;
+
+    @BeforeClass
+    public static void assumeMongoDB() {
+        assumeTrue(MongoUtils.isAvailable());
+    }
+
+    @Before
+    public void before() {
+        ns = createDocumentNodeStore();
+    }
+
+    @After
+    public void after() {
+        try {
+            closer.close();
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+        ns.dispose();
+    }
+
+    private DocumentNodeStore createDocumentNodeStore() {
+        MongoConnection c = connectionFactory.getConnection();
+        assertNotNull(c);
+        MongoUtils.dropCollections(c.getDatabase());
+        return builderProvider.newBuilder().setFullGCEnabled(false).setLeaseCheckMode(LeaseCheckMode.DISABLED).setAsyncDelay(0)
+                .setMongoDB(c.getMongoClient(), c.getDBName()).getNodeStore();
+    }
+
+    private List<String> getChildNodeNames(NodeState node) {
+        List<String> childNodeNames = new ArrayList<>();
+        for (String childNodeName : node.getChildNodeNames()) {
+            childNodeNames.add(childNodeName);
+        }
+        return childNodeNames;
+    }
+
+    /**
+     * This test generates garbage empty properties under one parent node and then cleans up the garbage.
+     * Use a custom MongoDB connection string for the MongoDB to run the test on.
+     * Long running test, should only be run manually.
+     */
+    @Ignore
+    @Test
+    public void generateGarbageEmptyPropsUnderOneParentOnCustomMongoDB() {
+        ns.dispose();
+
+        // this should be a valid mongoDB connection string for the MongoDB on which to generate the garbage
+        String mongoDBConnString = "";
+        CreateGarbageCmd cmd = new CreateGarbageCmd( mongoDBConnString, OPTION_GARBAGE_NODES_COUNT, "10", OPTION_GARBAGE_NODES_PARENT_COUNT, "1",
+                OPTION_GARBAGE_TYPE, "1");
+        String output = captureSystemOut(cmd);
+
+        //sleep thread for 10 seconds to allow the garbage generation to complete
+        try {
+            Thread.sleep(10000);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+
+        DocumentNodeStore nodeStore = cmd.getCommand().getDocumentNodeStore();
+        NodeState garbageRootNodeState = getGarbageRootNode(nodeStore);
+        List<String> garbageRootNodeChildNames = getChildNodeNames(garbageRootNodeState);
+
+        assertEquals(garbageRootNodeChildNames.size(), 1);
+        List<String> propParentsGarbageNodeNames = getChildNodeNames(garbageRootNodeState.getChildNode(garbageRootNodeChildNames.get(0)));
+        assertEquals(propParentsGarbageNodeNames.size(), 1);
+
+        List<String> propGarbageNodeNames = getChildNodeNames(garbageRootNodeState.getChildNode(garbageRootNodeChildNames.get(0)).getChildNode(propParentsGarbageNodeNames.get(0)));
+        assertEquals(propGarbageNodeNames.size(), 10);
+    }
+
+    /**
+     * This test generates garbage empty properties and then cleans up the garbage.
+     * Long running test, should only be run manually.
+     */
+    @Ignore
+    @Test
+    public void cleanupAllGarbageEmptyProperties() {
+
+        // generate garbage first
+        CreateGarbageCmd cmdGenerateGarbage = new CreateGarbageCmd(OPTION_GARBAGE_NODES_COUNT, "4", OPTION_GARBAGE_NODES_PARENT_COUNT, "3",
+                OPTION_GARBAGE_TYPE, "1");
+        cmdGenerateGarbage.run();
+        Closer cmd1Closer = cmdGenerateGarbage.getCloser();
+
+        // cleanup garbage
+        CreateGarbageCmd cmdClean = new CreateGarbageCmd("clean");
+        cmdClean.run();
+        closer = cmdClean.getCloser();
+
+        NodeDocument garbageRoot = getDocument(cmdClean, Collection.NODES, Utils.getIdFromPath("/" + CreateGarbageCommand.GARBAGE_GEN_ROOT_PATH), 0);
+        // garbage root node should be either deleted or empty (no children)
+        assertTrue(garbageRoot == null || !garbageRoot.hasChildren());
+
+        try {
+            cmd1Closer.close();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * This test generates garbage gap orphans and then cleans up the garbage.
+     * Long running test, should only be run manually.
+     */
+    @Ignore
+    @Test
+    public void cleanupAllGarbageGapOrphans() {
+
+        // generate garbage first
+        CreateGarbageCmd cmdGenerateGarbage = new CreateGarbageCmd(OPTION_GARBAGE_NODES_COUNT, "10", OPTION_GARBAGE_NODES_PARENT_COUNT, "1",
+                OPTION_GARBAGE_TYPE, "2");
+        cmdGenerateGarbage.run();
+        Closer cmd1Closer = cmdGenerateGarbage.getCloser();
+
+        // cleanup garbage
+        CreateGarbageCmd cmdClean = new CreateGarbageCmd("clean");
+        cmdClean.run();
+        closer = cmdClean.getCloser();
+
+        NodeDocument garbageRoot = getDocument(cmdClean, Collection.NODES, Utils.getIdFromPath("/" + CreateGarbageCommand.GARBAGE_GEN_ROOT_PATH), 0);
+        // garbage root node should be either deleted or empty (no children)
+        assertTrue(garbageRoot == null || !garbageRoot.hasChildren());
+
+        try {
+            cmd1Closer.close();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Test
+    public void generateGarbageEmptyPropsUnderOneParent() {
+        ns.dispose();
+
+        CreateGarbageCmd cmd = new CreateGarbageCmd(OPTION_GARBAGE_NODES_COUNT, "10", OPTION_GARBAGE_NODES_PARENT_COUNT, "1",
+                OPTION_GARBAGE_TYPE, "1");
+        String output = captureSystemOut(cmd);
+        closer = cmd.getCloser();
+
+        List<String> generatedBasePaths = cmd.getGeneratedBasePaths();
+
+        DocumentNodeStore nodeStore = cmd.getCommand().getDocumentNodeStore();
+        NodeState garbageRootNodeState = getGarbageRootNode(nodeStore);
+        List<String> garbageRootNodeChildNames = getChildNodeNames(garbageRootNodeState);
+
+        assertEquals(garbageRootNodeChildNames.size(), 1);
+        for (String generateBasePath : generatedBasePaths) {
+            assertTrue(garbageRootNodeChildNames.contains(generateBasePath));
+        }
+
+        List<String> propParentsGarbageNodeNames = getChildNodeNames(garbageRootNodeState.getChildNode(garbageRootNodeChildNames.get(0)));
+        assertEquals(propParentsGarbageNodeNames.size(), 1);
+
+        NodeState emptyPropsParentNode = garbageRootNodeState.getChildNode(garbageRootNodeChildNames.get(0)).getChildNode(propParentsGarbageNodeNames.get(0));
+        List<String> propGarbageNodeNames = getChildNodeNames(emptyPropsParentNode);
+        assertEquals(propGarbageNodeNames.size(), 10);
+
+        for (String propGarbageNodeName : propGarbageNodeNames) {
+            NodeState propGarbageNode = emptyPropsParentNode.getChildNode(propGarbageNodeName);
+            assertNull(propGarbageNode.getProperty(EMPTY_PROPERTY_NAME));
+        }
+    }
+
+    @Test
+    public void generateGarbageGapOrphansUnderOneParent() {
+        ns.dispose();
+
+        int garbageNodesCount = 10;
+        int garbageNodesParentCount = 1;
+
+        testGenerateGapOrphans(garbageNodesCount, garbageNodesParentCount);
+    }
+
+    @Test
+    public void generateGarbageGapOrphansUnderMultipleParents() {
+        ns.dispose();
+
+        int garbageNodesCount = 20;
+        int garbageNodesParentCount = 4;
+
+        testGenerateGapOrphans(garbageNodesCount, garbageNodesParentCount);
+    }
+
+    @Test
+    public void generateGarbageGapOrphansWithDepthAndMinLevelGap() {
+        ns.dispose();
+
+        int garbageNodesCount = 5;
+        int garbageNodesParentCount = 1;
+        int orphansDepth = 7;
+        int orphansLevelGap = 0;
+
+        testGenerateGapOrphans(garbageNodesCount, garbageNodesParentCount, orphansDepth, orphansLevelGap);
+    }
+
+    @Test
+    public void generateGarbageGapOrphansWithDepthAndMaxLevelMinusOneGap() {
+        ns.dispose();
+
+        int garbageNodesCount = 1;
+        int garbageNodesParentCount = 1;
+        int orphansDepth = 10;
+        int orphansLevelGap = 9;
+
+        testGenerateGapOrphans(garbageNodesCount, garbageNodesParentCount, orphansDepth, orphansLevelGap);
+    }
+
+    @Test
+    public void generateGarbageGapOrphansWithDepthAndMaxLevelGap() {
+        ns.dispose();
+
+        int garbageNodesCount = 5;
+        int garbageNodesParentCount = 1;
+        int orphansDepth = 11;
+        int orphansLevelGap = 11;
+
+        testGenerateGapOrphans(garbageNodesCount, garbageNodesParentCount, orphansDepth, orphansLevelGap);
+    }
+
+    @Test
+    public void generateGarbageGapOrphansWithDepthAndMiddleLevelGap() {
+        ns.dispose();
+
+        int garbageNodesCount = 5;
+        int garbageNodesParentCount = 1;
+        int orphansDepth = 8;
+        int orphansLevelGap = 4;
+
+        testGenerateGapOrphans(garbageNodesCount, garbageNodesParentCount, orphansDepth, orphansLevelGap);
+    }
+
+    /**
+     * Calls the GenerateFullGCCommand to generate gap orphans and verifies the generated gap orphans were created
+     * correctly.
+     * Gap orphan depth is minimum and level gap is 0.
+     * @param garbageNodesCount
+     * @param garbageNodesParentCount
+     */
+    private void testGenerateGapOrphans(int garbageNodesCount, int garbageNodesParentCount) {
+        testGenerateGapOrphans(garbageNodesCount, garbageNodesParentCount, 1, 0);
+    }
+
+    /**
+     * Calls the GenerateFullGCCommand to generate gap orphans and verifies the generated gap orphans:
+     * - checks that parents of gap orphans are missing by retrieving the nodes from the document store
+     * - checks that the expected number gap orphans are generated by retrieving the nodes from the document store
+     * @param garbageNodesCount
+     * @param garbageNodesParentCount
+     * @param orphansDepth - the depth of the leaf garbage gap orphan node
+     * @param orphansLevelGap - the level that is used as threshold for deletion - all nodes from the parent to this node
+     *                        will be deleted
+     */
+    private void testGenerateGapOrphans(int garbageNodesCount, int garbageNodesParentCount,
+                                        int orphansDepth, int orphansLevelGap) {
+        CreateGarbageCmd cmd = new CreateGarbageCmd(OPTION_GARBAGE_NODES_COUNT, String.valueOf(garbageNodesCount),
+                OPTION_GARBAGE_NODES_PARENT_COUNT, String.valueOf(garbageNodesParentCount),
+                OPTION_GARBAGE_ORPHANS_DEPTH, String.valueOf(orphansDepth),
+                OPTION_GARBAGE_ORPHANS_LEVEL_GAP, String.valueOf(orphansLevelGap),
+                OPTION_GARBAGE_TYPE, "2");
+        String output = captureSystemOut(cmd);
+        closer = cmd.getCloser();
+
+        int nodesPerParent = garbageNodesCount / garbageNodesParentCount;
+
+        List<String> generatedBasePaths = cmd.getGeneratedBasePaths();
+
+        DocumentNodeStore nodeStore = cmd.getCommand().getDocumentNodeStore();
+        NodeState garbageRootNodeState = getGarbageRootNode(nodeStore);
+        List<String> garbageRootNodeChildNames = getChildNodeNames(garbageRootNodeState);
+
+        assertEquals(garbageRootNodeChildNames.size(), 1);
+        for (String generateBasePath : generatedBasePaths) {
+            assertTrue(garbageRootNodeChildNames.contains(generateBasePath));
+        }
+
+        String generateBasePath = generatedBasePaths.get(0);
+        for (int i = 0; i < garbageNodesParentCount; i ++) {
+
+            for (int j = 0; j < nodesPerParent; j++) {
+
+                // check deleted node paths
+                List<String> deletedNodePaths = CreateGarbageCommand.generateGapOrphanNodePaths(
+                        generateBasePath, i, j, orphansDepth, orphansLevelGap, false);
+                for (String deletedNodePath : deletedNodePaths) {
+                    NodeDocument missingDocument = getDocument(cmd, Collection.NODES, deletedNodePath, 0);
+                    assertNull(missingDocument);
+                }
+
+                // check not deleted node paths - remaining gap orphans
+                List<String> notDeletedNodePaths = CreateGarbageCommand.generateGapOrphanNodePaths(generateBasePath, i, j, orphansDepth, orphansLevelGap, true);
+                for (String notDeletedNodePath : notDeletedNodePaths) {
+                    NodeDocument gapOrphanDocument = getDocument(cmd, Collection.NODES, notDeletedNodePath, 0);
+                    assertNotNull(gapOrphanDocument);
+                }
+            }
+        }
+    }
+
+    /**
+     * Utility method that returns the root node of the full gc generated garbage nodes.
+     * @param nodeStore
+     * @return
+     */
+    private static @NotNull NodeState getGarbageRootNode(DocumentNodeStore nodeStore) {
+        NodeState garbageRootNodeState = nodeStore.getRoot().getChildNode(CreateGarbageCommand.GARBAGE_GEN_ROOT_PATH_BASE).
+                getChildNode(CreateGarbageCommand.GARBAGE_GEN_ROOT_NODE_NAME);
+        return garbageRootNodeState;
+    }
+
+    /**
+     * Used in unit tests for retrieving a document by path from the document store using the documentNodeStore
+     * of this class.
+     * @param collection
+     * @param key
+     * @param maxCacheAge
+     * @return
+     * @param <T>
+     * @throws DocumentStoreException
+     */
+    @Nullable
+    private  <T extends Document> T getDocument(CreateGarbageCmd cmd, Collection<T> collection, String key, int maxCacheAge)
+            throws DocumentStoreException {
+        return cmd.getCommand().getDocumentNodeStore().getDocumentStore().find(collection, key, maxCacheAge);
+    }
+
+    /**
+     * Wrapper class for the GenerateGarbageCommand to be used in unit tests.
+     */
+    private static class CreateGarbageCmd implements Runnable {
+
+        private final ImmutableList<String> args;
+        private CreateGarbageCommand command;
+        private Closer closer;
+        private List<String> generatedBasePaths;
+
+        public CreateGarbageCmd(String... args) {
+            // append the default mongodb connection string if one was not provided
+            if (args[0].startsWith("mongodb://")) {
+                this.args = ImmutableList.copyOf(args);
+            } else {
+                this.args = ImmutableList.<String>builder().add(MongoUtils.URL)

Review Comment:
   same as above.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "shodaaan (via GitHub)" <gi...@apache.org>.
shodaaan commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1832475642


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/CreateGarbageCommand.java:
##########
@@ -0,0 +1,574 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.base.Joiner;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.commons.concurrent.ExecutorCloser;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.GenerateGarbageHelper;
+import org.apache.jackrabbit.oak.plugins.document.LeaseCheckMode;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.asCloseable;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateGarbageCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class CreateGarbageCommand implements Command, Closeable {
+    private static final Logger LOG = LoggerFactory.getLogger(CreateGarbageCommand.class);
+
+    private final ScheduledExecutorService continuousRunExecutor = Executors.newScheduledThreadPool(1);
+
+    private static final String USAGE = Joiner.on(System.lineSeparator()).join(
+            "create-test-garbage {<jdbc-uri> | <mongodb-uri>} <sub-command> [options] ",
+            "where sub-command is one of: ",
+            "  clean - clean up all generated garbage under the specified root node",
+            "  generate - generate garbage nodes in the repository, under the root node tmp/oak-run-generated-test-garbage. ",
+            "           Use the --garbageType (required) option to specify the type of garbage to generate",
+            "           the --garbageNodesCount option to specify the total number of garbage nodes to create,",
+            "           the --garbageNodesParentCount option to specify the total number of parent nodes under which to create garbage nodes,",
+            "           the --orphansDepth option to specify the depth in the tree at which to create garbage nodes,",
+            "           the --orphansLevelGap option to specify the gap in the tree between the first leaf gap orphan garbage node and its parent,",
+            "           the --numberOfRuns option to specify the number of garbage generation runs to do,",
+            "           and the --generateIntervalSeconds option to specify the interval at which to generate a complete garbage count from createGarbageNotesCount.");
+
+
+    /**
+     * Garbage should be generated under tmp node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH_BASE = "tmp";
+
+    /**
+     * The root node name for garbage generation, one level under tmp.
+     */
+    public static String GARBAGE_GEN_ROOT_NODE_NAME = "oak-run-generated-test-garbage";
+
+    /**
+     * Root node for garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH = GARBAGE_GEN_ROOT_PATH_BASE + "/" + GARBAGE_GEN_ROOT_NODE_NAME;
+
+    /**
+     * Base path for garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String GEN_BASE_PATH = "GenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String GEN_PARENT_NODE_PREFIX = "Parent_";
+    public static String GEN_NODE_PREFIX = "Node_";
+    public static String GEN_NODE_LEVEL_PREFIX = "_Level_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    /**
+     * The maximum depth in the tree at which to generate gap orphans garbage nodes.
+     * Used for validation of the orphansDepth / orphansLevelGap options.
+     */
+    private final int GAP_ORPHANS_MAX_DEPTH = 15;
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class CreateGarbageOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node GARBAGE_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> orphansDepth;
+        final OptionSpec<Integer> orphansLevelGap;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public CreateGarbageOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            orphansDepth = parser
+                    .accepts("orphansDepth", "the depth in the tree at which to create garbage nodes. Only applies to ORPHANS fullGC modes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            orphansLevelGap = parser
+                    .accepts("orphansLevelGap", "the gap in the tree between the first leaf gap orphan garbage node and its' parent. Only applies to ORPHANS fullGC modes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public CreateGarbageOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getOrphansDepth() { return orphansDepth.value(options);}
+
+        public int getOrphansLevelGap() { return orphansLevelGap.value(options); }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {
+
+        try (Closer closer = Closer.create()) {
+            // Register the ExecutorService with Closer
+            closer.register(() -> {
+                // Properly shutdown the executor service
+                continuousRunExecutor.shutdown();
+                try {
+                    if (!continuousRunExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
+                        continuousRunExecutor.shutdownNow();
+                    }
+                } catch (InterruptedException e) {
+                    throw new RuntimeException(e);
+                }
+            });
+
+            execute(closer, args);
+
+        } catch (Throwable e) {
+            LOG.error("Command failed", e);
+            throw e;
+        }
+    }
+
+    /**
+     * Method with passed closer is necessary in order to allow for unit tests to check the output of the command.
+     * It is the responsibility of the caller to close the closer.
+     *
+     * Returns the list of generated garbage base paths (under the garbage root node).
+     * @param closer
+     * @param args
+     * @throws Exception
+     */
+    public List<String> execute(Closer closer, String... args) throws Exception {
+        continuousRunIndex = 0;
+
+        List<String> generateBasePaths = new ArrayList<>();
+
+        CreateGarbageOptions options = new CreateGarbageOptions(USAGE).parse(args);
+        String subCmd = options.getSubCmd();
+
+        LOG.info("Executing command: " + getClass().getName() + " with sub-command: " + subCmd);
+
+        if (CreateGarbageOptions.CMD_GENERATE.equals(subCmd)) {
+
+            if (GenerateGarbageHelper.isInvalidGarbageGenerationMode(options.getGarbageType())) {
+                LOG.error("Invalid fullGCMode specified: " + options.getGarbageType() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+            if (options.getCreateGarbageNodesCount() <= 0) {
+                LOG.error("Invalid garbageNodesCount specified: " + options.getCreateGarbageNodesCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+            if (options.getGarbageNodesParentCount() <= 0) {
+                LOG.error("Invalid garbageNodesParentCount specified: " + options.getGarbageNodesParentCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+
+            if (options.getNumberOfRuns() > 1 && options.getGenerateIntervalSeconds() > 0) {
+                generateBasePaths.addAll(generateGarbageContinuously(options, closer));
+            } else {
+                generateBasePaths.add(createGarbage(options, closer, 0));
+            }
+        } else if (CreateGarbageOptions.CMD_CLEAN.equals(subCmd)) {
+            cleanGarbage(options, closer);
+        } else {
+            System.err.println("Unknown revisions command: " + subCmd);
+        }
+
+        return generateBasePaths;
+    }
+
+    @Override
+    public void close() throws IOException {

Review Comment:
   Done, thank you for the suggestion.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "shodaaan (via GitHub)" <gi...@apache.org>.
shodaaan commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1832475024


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/CreateGarbageCommand.java:
##########
@@ -0,0 +1,574 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.base.Joiner;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.commons.concurrent.ExecutorCloser;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.GenerateGarbageHelper;
+import org.apache.jackrabbit.oak.plugins.document.LeaseCheckMode;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.asCloseable;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateGarbageCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class CreateGarbageCommand implements Command, Closeable {
+    private static final Logger LOG = LoggerFactory.getLogger(CreateGarbageCommand.class);
+
+    private final ScheduledExecutorService continuousRunExecutor = Executors.newScheduledThreadPool(1);
+
+    private static final String USAGE = Joiner.on(System.lineSeparator()).join(
+            "create-test-garbage {<jdbc-uri> | <mongodb-uri>} <sub-command> [options] ",
+            "where sub-command is one of: ",
+            "  clean - clean up all generated garbage under the specified root node",
+            "  generate - generate garbage nodes in the repository, under the root node tmp/oak-run-generated-test-garbage. ",

Review Comment:
   Done, thank you for the suggestion.



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/CreateGarbageCommand.java:
##########
@@ -0,0 +1,574 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.base.Joiner;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.commons.concurrent.ExecutorCloser;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.GenerateGarbageHelper;
+import org.apache.jackrabbit.oak.plugins.document.LeaseCheckMode;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.asCloseable;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateGarbageCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class CreateGarbageCommand implements Command, Closeable {
+    private static final Logger LOG = LoggerFactory.getLogger(CreateGarbageCommand.class);
+
+    private final ScheduledExecutorService continuousRunExecutor = Executors.newScheduledThreadPool(1);
+
+    private static final String USAGE = Joiner.on(System.lineSeparator()).join(
+            "create-test-garbage {<jdbc-uri> | <mongodb-uri>} <sub-command> [options] ",
+            "where sub-command is one of: ",
+            "  clean - clean up all generated garbage under the specified root node",
+            "  generate - generate garbage nodes in the repository, under the root node tmp/oak-run-generated-test-garbage. ",
+            "           Use the --garbageType (required) option to specify the type of garbage to generate",

Review Comment:
   Done, thank you for the suggestion.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "shodaaan (via GitHub)" <gi...@apache.org>.
shodaaan commented on PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#issuecomment-2471008527

   > Had troubles getting the continuous mode running and I believe it's broken because of the new closer logic closing it early.
   > 
   > (other than that added a few detail remarks as well)
   
   Tested oak-run with all modes (including continuous generation), everything works fine now.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "stefan-egli (via GitHub)" <gi...@apache.org>.
stefan-egli commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1836941746


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/plugins/document/CreateGarbageHelper.java:
##########
@@ -0,0 +1,37 @@
+/*
+ * 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;
+
+/**
+ * Helper class to generate garbage for testing purposes.
+ */
+public class CreateGarbageHelper {
+
+    public static boolean isInvalidGarbageGenerationMode(int fullGCMode) {
+        return fullGCMode == 0;
+    }
+
+    public static boolean isEmptyProps(int fullGCMode) {
+        return fullGCMode == 1;
+    }
+
+    public static boolean isGapOrphans(int fullGCMode) {
+        return fullGCMode == 2;
+    }

Review Comment:
   I think it's ok to have these for now (except they are duplicated by FullGCHelper currently).
   
   I do think though that we could move them to oak-store-document's VersionGarbageCollector - or actually make the FullGCMode in there a bit more than just an enum (or some other reasonable refactoring).
   
   but I think the refactoring can be done later and doesn't have to block this PR



##########
oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/FullGCHelper.java:
##########
@@ -70,6 +70,14 @@ public static RevisionVector unmergedBranchCommit(final NodeStore store, final C
         return result;
     }
 
+    public static boolean includesEmptyProps(int fullGCMode) {
+        return fullGCMode == 1;
+    }
+
+    public static boolean includesGapOrphans(int fullGCMode) {
+        return fullGCMode == 2;
+    }
+

Review Comment:
   these methods are not used. would suggest to either remove them or consolidate them with the same in CreateGarbageHelper



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/CreateGarbageCommand.java:
##########
@@ -0,0 +1,568 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.base.Joiner;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.CreateGarbageHelper;
+import org.apache.jackrabbit.oak.plugins.document.LeaseCheckMode;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.asCloseable;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * CreateGarbageCommand creates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class CreateGarbageCommand implements Command {
+    private static final Logger LOG = LoggerFactory.getLogger(CreateGarbageCommand.class);
+
+    private final ScheduledExecutorService continuousRunExecutor = Executors.newScheduledThreadPool(1);
+
+    private static final String USAGE = Joiner.on(System.lineSeparator()).join(
+            "create-test-garbage {<jdbc-uri> | <mongodb-uri>} <sub-command> [options] ",
+            "where sub-command is one of: ",
+            "  clean - clean up all generated garbage under the specified root node",
+            "  create - create garbage nodes in the repository, under the root node tmp/oak-run-generated-test-garbage. ",
+            "           Use the --garbageType (required) option to specify the type of garbage to create",
+            "           the --garbageNodesCount option to specify the total number of garbage nodes to create,",
+            "           the --garbageNodesParentCount option to specify the total number of parent nodes under which to create garbage nodes,",
+            "           the --orphansDepth option to specify the depth in the tree at which to create garbage nodes,",
+            "           the --orphansLevelGap option to specify the gap in the tree between the first leaf gap orphan garbage node and its parent,",
+            "           the --numberOfRuns option to specify the number of garbage generation runs to do,",
+            "           and the --generateIntervalSeconds option to specify the interval at which to generate a complete garbage count from createGarbageNotesCount.");
+
+
+    /**
+     * Garbage should be generated under tmp node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH_BASE = "tmp";
+
+    /**
+     * The root node name for garbage generation, one level under tmp.
+     */
+    public static String GARBAGE_GEN_ROOT_NODE_NAME = "oak-run-generated-test-garbage";
+
+    /**
+     * Root node for garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH = GARBAGE_GEN_ROOT_PATH_BASE + "/" + GARBAGE_GEN_ROOT_NODE_NAME;
+
+    /**
+     * Base path for garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String GEN_BASE_PATH = "GenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String GEN_PARENT_NODE_PREFIX = "Parent_";
+    public static String GEN_NODE_PREFIX = "Node_";
+    public static String GEN_NODE_LEVEL_PREFIX = "_Level_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    /**
+     * The maximum depth in the tree at which to generate gap orphans garbage nodes.
+     * Used for validation of the orphansDepth / orphansLevelGap options.
+     */
+    private final int GAP_ORPHANS_MAX_DEPTH = 15;
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class CreateGarbageOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_CREATE = "create";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node GARBAGE_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> orphansDepth;
+        final OptionSpec<Integer> orphansLevelGap;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public CreateGarbageOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            orphansDepth = parser
+                    .accepts("orphansDepth", "the depth in the tree at which to create garbage nodes. Only applies to ORPHANS fullGC modes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            orphansLevelGap = parser
+                    .accepts("orphansLevelGap", "the gap in the tree between the first leaf gap orphan garbage node and its' parent. Only applies to ORPHANS fullGC modes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public CreateGarbageOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_CREATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getOrphansDepth() { return orphansDepth.value(options);}
+
+        public int getOrphansLevelGap() { return orphansLevelGap.value(options); }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    @Override
+    public void execute(String... args) throws Exception {
+
+        try (Closer closer = Closer.create()) {
+            // Register the ExecutorService with Closer
+            closer.register(() -> {
+                // Properly shutdown the executor service
+                continuousRunExecutor.shutdown();
+                try {
+                    if (!continuousRunExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
+                        continuousRunExecutor.shutdownNow();
+                    }
+                } catch (InterruptedException e) {
+                    throw new RuntimeException(e);
+                }
+            });
+
+            execute(closer, args);
+

Review Comment:
   when used in continuous mode (eg by setting `--numberOfRuns 42`) then the code here terminates early and closes the Closer - preventing the continuous mode from practically ever running



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/CreateGarbageCommand.java:
##########
@@ -0,0 +1,568 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.base.Joiner;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.CreateGarbageHelper;
+import org.apache.jackrabbit.oak.plugins.document.LeaseCheckMode;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.asCloseable;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * CreateGarbageCommand creates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class CreateGarbageCommand implements Command {
+    private static final Logger LOG = LoggerFactory.getLogger(CreateGarbageCommand.class);
+
+    private final ScheduledExecutorService continuousRunExecutor = Executors.newScheduledThreadPool(1);
+
+    private static final String USAGE = Joiner.on(System.lineSeparator()).join(
+            "create-test-garbage {<jdbc-uri> | <mongodb-uri>} <sub-command> [options] ",
+            "where sub-command is one of: ",
+            "  clean - clean up all generated garbage under the specified root node",
+            "  create - create garbage nodes in the repository, under the root node tmp/oak-run-generated-test-garbage. ",
+            "           Use the --garbageType (required) option to specify the type of garbage to create",
+            "           the --garbageNodesCount option to specify the total number of garbage nodes to create,",
+            "           the --garbageNodesParentCount option to specify the total number of parent nodes under which to create garbage nodes,",
+            "           the --orphansDepth option to specify the depth in the tree at which to create garbage nodes,",
+            "           the --orphansLevelGap option to specify the gap in the tree between the first leaf gap orphan garbage node and its parent,",
+            "           the --numberOfRuns option to specify the number of garbage generation runs to do,",
+            "           and the --generateIntervalSeconds option to specify the interval at which to generate a complete garbage count from createGarbageNotesCount.");
+
+
+    /**
+     * Garbage should be generated under tmp node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH_BASE = "tmp";
+
+    /**
+     * The root node name for garbage generation, one level under tmp.
+     */
+    public static String GARBAGE_GEN_ROOT_NODE_NAME = "oak-run-generated-test-garbage";
+
+    /**
+     * Root node for garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH = GARBAGE_GEN_ROOT_PATH_BASE + "/" + GARBAGE_GEN_ROOT_NODE_NAME;
+
+    /**
+     * Base path for garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String GEN_BASE_PATH = "GenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String GEN_PARENT_NODE_PREFIX = "Parent_";
+    public static String GEN_NODE_PREFIX = "Node_";
+    public static String GEN_NODE_LEVEL_PREFIX = "_Level_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    /**
+     * The maximum depth in the tree at which to generate gap orphans garbage nodes.
+     * Used for validation of the orphansDepth / orphansLevelGap options.
+     */
+    private final int GAP_ORPHANS_MAX_DEPTH = 15;
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class CreateGarbageOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_CREATE = "create";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node GARBAGE_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> orphansDepth;
+        final OptionSpec<Integer> orphansLevelGap;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public CreateGarbageOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            orphansDepth = parser
+                    .accepts("orphansDepth", "the depth in the tree at which to create garbage nodes. Only applies to ORPHANS fullGC modes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            orphansLevelGap = parser
+                    .accepts("orphansLevelGap", "the gap in the tree between the first leaf gap orphan garbage node and its' parent. Only applies to ORPHANS fullGC modes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public CreateGarbageOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_CREATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getOrphansDepth() { return orphansDepth.value(options);}
+
+        public int getOrphansLevelGap() { return orphansLevelGap.value(options); }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    @Override
+    public void execute(String... args) throws Exception {
+
+        try (Closer closer = Closer.create()) {
+            // Register the ExecutorService with Closer
+            closer.register(() -> {
+                // Properly shutdown the executor service
+                continuousRunExecutor.shutdown();
+                try {
+                    if (!continuousRunExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
+                        continuousRunExecutor.shutdownNow();
+                    }
+                } catch (InterruptedException e) {
+                    throw new RuntimeException(e);
+                }
+            });
+
+            execute(closer, args);
+
+        } catch (Throwable e) {
+            LOG.error("Command failed", e);
+            throw e;
+        }
+    }
+
+    /**
+     * Method with passed closer is necessary in order to allow for unit tests to check the output of the command.
+     * It is the responsibility of the caller to close the closer.
+     *
+     * Returns the list of generated garbage base paths (under the garbage root node).
+     * @param closer
+     * @param args
+     * @throws Exception
+     */
+    public List<String> execute(Closer closer, String... args) throws Exception {
+        continuousRunIndex = 0;
+
+        List<String> generateBasePaths = new ArrayList<>();
+
+        CreateGarbageOptions options = new CreateGarbageOptions(USAGE).parse(args);
+        String subCmd = options.getSubCmd();
+
+        LOG.info("Executing command: " + getClass().getName() + " with sub-command: " + subCmd);
+
+        if (CreateGarbageOptions.CMD_CREATE.equals(subCmd)) {
+
+            if (CreateGarbageHelper.isInvalidGarbageGenerationMode(options.getGarbageType())) {
+                LOG.error("Invalid fullGCMode specified: " + options.getGarbageType() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+            if (options.getCreateGarbageNodesCount() <= 0) {
+                LOG.error("Invalid garbageNodesCount specified: " + options.getCreateGarbageNodesCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+            if (options.getGarbageNodesParentCount() <= 0) {
+                LOG.error("Invalid garbageNodesParentCount specified: " + options.getGarbageNodesParentCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+
+            if (options.getNumberOfRuns() > 1 && options.getGenerateIntervalSeconds() > 0) {
+                generateBasePaths.addAll(generateGarbageContinuously(options, closer));
+            } else {
+                generateBasePaths.add(createGarbage(options, closer, 0));
+            }
+        } else if (CreateGarbageOptions.CMD_CLEAN.equals(subCmd)) {
+            cleanGarbage(options, closer);
+        } else {
+            System.err.println("Unknown revisions command: " + subCmd);
+        }
+
+        return generateBasePaths;
+    }
+
+    private List<String> generateGarbageContinuously(CreateGarbageOptions options, Closer closer) throws Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long startGenTimestamp = System.currentTimeMillis();
+
+        List<String> generatedGarbageBasePaths = new ArrayList<>();
+
+        int numberOfRuns = options.getNumberOfRuns();
+        int intervalSeconds = options.getGenerateIntervalSeconds();
+        Runnable task = () -> {
+            if (continuousRunIndex < numberOfRuns) {
+                try {
+                    String genBasePath = createGarbage(options, closer, continuousRunIndex, builder, startGenTimestamp);
+                    generatedGarbageBasePaths.add(genBasePath);
+                } catch (Exception e) {
+                    LOG.error("Error generating garbage in run " + continuousRunIndex, e);
+                }
+                LOG.info("Task executed. Count: " + (continuousRunIndex + 1));
+                continuousRunIndex++;
+            } else {
+                // Shutdown the executor once the task has run numberOfRuns times
+                continuousRunExecutor.shutdown();
+                LOG.info("Task completed " + numberOfRuns + " times. Stopping execution.");
+            }
+        };
+
+        // Schedule the task to run every intervalSeconds
+        continuousRunExecutor.scheduleAtFixedRate(task, 0, intervalSeconds, TimeUnit.SECONDS);
+
+        return generatedGarbageBasePaths;
+    }
+
+    /**
+     * Generate garbage nodes in the repository in order to allow for testing fullGC functionality.
+     *
+     * Returns the path of the generated GARBAGE_GEN_ROOT_PATH node (under the root).
+     * @param options
+     * @param closer
+     * @param runIndex
+     * @return
+     * @throws IOException
+     * @throws Exception
+     */
+    private String createGarbage(CreateGarbageOptions options, Closer closer, int runIndex) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long generationTimestamp = System.currentTimeMillis();
+
+        return createGarbage(options, closer, runIndex, builder, generationTimestamp);
+    }
+
+    private String createGarbage(CreateGarbageOptions options, Closer closer, int runIndex,
+                                 DocumentNodeStoreBuilder<?> builder, long timestamp) throws Exception {
+
+        if (builder == null) {
+            System.err.println("generateGarbage mode only available for DocumentNodeStore");
+            System.exit(1);
+        }
+
+        String generationBasePath = GEN_BASE_PATH + timestamp + "_" + runIndex;
+        documentNodeStore = builder.build();
+        closer.register(asCloseable(documentNodeStore));
+
+        NodeBuilder rootNode = documentNodeStore.getRoot().builder();

Review Comment:
   nitpick : it could be somewhat confusing if a `NodeBuilder` is called `somethingNode`. Usually we call it `somethingBuilder` or `something` (as `somethingNode` is typically used for a `NodeState`)
   (elsewhere I see you're also using the classic terminology, so this applies only to part of the code)



##########
oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/VersionGarbageCollector.java:
##########
@@ -168,7 +168,7 @@ public class VersionGarbageCollector {
      * Ultimately the goal is to clean up all possible garbage. After hardening these modes
      * might no longer be supported.
      */
-    enum FullGCMode {
+     enum FullGCMode {

Review Comment:
   nitpick, detail, but I would remove this, it is an unnecessary change



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/CreateGarbageCommand.java:
##########
@@ -0,0 +1,568 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.base.Joiner;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.CreateGarbageHelper;
+import org.apache.jackrabbit.oak.plugins.document.LeaseCheckMode;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.asCloseable;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * CreateGarbageCommand creates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class CreateGarbageCommand implements Command {
+    private static final Logger LOG = LoggerFactory.getLogger(CreateGarbageCommand.class);
+
+    private final ScheduledExecutorService continuousRunExecutor = Executors.newScheduledThreadPool(1);
+
+    private static final String USAGE = Joiner.on(System.lineSeparator()).join(
+            "create-test-garbage {<jdbc-uri> | <mongodb-uri>} <sub-command> [options] ",
+            "where sub-command is one of: ",
+            "  clean - clean up all generated garbage under the specified root node",
+            "  create - create garbage nodes in the repository, under the root node tmp/oak-run-generated-test-garbage. ",
+            "           Use the --garbageType (required) option to specify the type of garbage to create",
+            "           the --garbageNodesCount option to specify the total number of garbage nodes to create,",
+            "           the --garbageNodesParentCount option to specify the total number of parent nodes under which to create garbage nodes,",
+            "           the --orphansDepth option to specify the depth in the tree at which to create garbage nodes,",
+            "           the --orphansLevelGap option to specify the gap in the tree between the first leaf gap orphan garbage node and its parent,",
+            "           the --numberOfRuns option to specify the number of garbage generation runs to do,",
+            "           and the --generateIntervalSeconds option to specify the interval at which to generate a complete garbage count from createGarbageNotesCount.");
+
+
+    /**
+     * Garbage should be generated under tmp node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH_BASE = "tmp";
+
+    /**
+     * The root node name for garbage generation, one level under tmp.
+     */
+    public static String GARBAGE_GEN_ROOT_NODE_NAME = "oak-run-generated-test-garbage";
+
+    /**
+     * Root node for garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH = GARBAGE_GEN_ROOT_PATH_BASE + "/" + GARBAGE_GEN_ROOT_NODE_NAME;
+
+    /**
+     * Base path for garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String GEN_BASE_PATH = "GenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String GEN_PARENT_NODE_PREFIX = "Parent_";
+    public static String GEN_NODE_PREFIX = "Node_";
+    public static String GEN_NODE_LEVEL_PREFIX = "_Level_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    /**
+     * The maximum depth in the tree at which to generate gap orphans garbage nodes.
+     * Used for validation of the orphansDepth / orphansLevelGap options.
+     */
+    private final int GAP_ORPHANS_MAX_DEPTH = 15;
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class CreateGarbageOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_CREATE = "create";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node GARBAGE_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> orphansDepth;
+        final OptionSpec<Integer> orphansLevelGap;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public CreateGarbageOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            orphansDepth = parser
+                    .accepts("orphansDepth", "the depth in the tree at which to create garbage nodes. Only applies to ORPHANS fullGC modes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            orphansLevelGap = parser
+                    .accepts("orphansLevelGap", "the gap in the tree between the first leaf gap orphan garbage node and its' parent. Only applies to ORPHANS fullGC modes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public CreateGarbageOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_CREATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getOrphansDepth() { return orphansDepth.value(options);}
+
+        public int getOrphansLevelGap() { return orphansLevelGap.value(options); }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    @Override
+    public void execute(String... args) throws Exception {
+
+        try (Closer closer = Closer.create()) {
+            // Register the ExecutorService with Closer
+            closer.register(() -> {
+                // Properly shutdown the executor service
+                continuousRunExecutor.shutdown();
+                try {
+                    if (!continuousRunExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
+                        continuousRunExecutor.shutdownNow();
+                    }
+                } catch (InterruptedException e) {
+                    throw new RuntimeException(e);
+                }
+            });
+
+            execute(closer, args);
+
+        } catch (Throwable e) {
+            LOG.error("Command failed", e);
+            throw e;
+        }
+    }
+
+    /**
+     * Method with passed closer is necessary in order to allow for unit tests to check the output of the command.
+     * It is the responsibility of the caller to close the closer.
+     *
+     * Returns the list of generated garbage base paths (under the garbage root node).
+     * @param closer
+     * @param args
+     * @throws Exception
+     */
+    public List<String> execute(Closer closer, String... args) throws Exception {
+        continuousRunIndex = 0;
+
+        List<String> generateBasePaths = new ArrayList<>();
+
+        CreateGarbageOptions options = new CreateGarbageOptions(USAGE).parse(args);
+        String subCmd = options.getSubCmd();
+
+        LOG.info("Executing command: " + getClass().getName() + " with sub-command: " + subCmd);
+
+        if (CreateGarbageOptions.CMD_CREATE.equals(subCmd)) {
+
+            if (CreateGarbageHelper.isInvalidGarbageGenerationMode(options.getGarbageType())) {
+                LOG.error("Invalid fullGCMode specified: " + options.getGarbageType() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+            if (options.getCreateGarbageNodesCount() <= 0) {
+                LOG.error("Invalid garbageNodesCount specified: " + options.getCreateGarbageNodesCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+            if (options.getGarbageNodesParentCount() <= 0) {
+                LOG.error("Invalid garbageNodesParentCount specified: " + options.getGarbageNodesParentCount() + " in: " + getClass().getName());
+                System.exit(1);
+            }
+
+            if (options.getNumberOfRuns() > 1 && options.getGenerateIntervalSeconds() > 0) {
+                generateBasePaths.addAll(generateGarbageContinuously(options, closer));
+            } else {
+                generateBasePaths.add(createGarbage(options, closer, 0));
+            }
+        } else if (CreateGarbageOptions.CMD_CLEAN.equals(subCmd)) {
+            cleanGarbage(options, closer);
+        } else {
+            System.err.println("Unknown revisions command: " + subCmd);
+        }
+
+        return generateBasePaths;
+    }
+
+    private List<String> generateGarbageContinuously(CreateGarbageOptions options, Closer closer) throws Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long startGenTimestamp = System.currentTimeMillis();
+
+        List<String> generatedGarbageBasePaths = new ArrayList<>();
+
+        int numberOfRuns = options.getNumberOfRuns();
+        int intervalSeconds = options.getGenerateIntervalSeconds();
+        Runnable task = () -> {
+            if (continuousRunIndex < numberOfRuns) {
+                try {
+                    String genBasePath = createGarbage(options, closer, continuousRunIndex, builder, startGenTimestamp);
+                    generatedGarbageBasePaths.add(genBasePath);
+                } catch (Exception e) {
+                    LOG.error("Error generating garbage in run " + continuousRunIndex, e);
+                }
+                LOG.info("Task executed. Count: " + (continuousRunIndex + 1));
+                continuousRunIndex++;
+            } else {
+                // Shutdown the executor once the task has run numberOfRuns times
+                continuousRunExecutor.shutdown();
+                LOG.info("Task completed " + numberOfRuns + " times. Stopping execution.");
+            }
+        };
+
+        // Schedule the task to run every intervalSeconds
+        continuousRunExecutor.scheduleAtFixedRate(task, 0, intervalSeconds, TimeUnit.SECONDS);
+
+        return generatedGarbageBasePaths;
+    }
+
+    /**
+     * Generate garbage nodes in the repository in order to allow for testing fullGC functionality.
+     *
+     * Returns the path of the generated GARBAGE_GEN_ROOT_PATH node (under the root).
+     * @param options
+     * @param closer
+     * @param runIndex
+     * @return
+     * @throws IOException
+     * @throws Exception
+     */
+    private String createGarbage(CreateGarbageOptions options, Closer closer, int runIndex) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long generationTimestamp = System.currentTimeMillis();
+
+        return createGarbage(options, closer, runIndex, builder, generationTimestamp);
+    }
+
+    private String createGarbage(CreateGarbageOptions options, Closer closer, int runIndex,
+                                 DocumentNodeStoreBuilder<?> builder, long timestamp) throws Exception {
+
+        if (builder == null) {
+            System.err.println("generateGarbage mode only available for DocumentNodeStore");
+            System.exit(1);
+        }
+
+        String generationBasePath = GEN_BASE_PATH + timestamp + "_" + runIndex;
+        documentNodeStore = builder.build();
+        closer.register(asCloseable(documentNodeStore));
+
+        NodeBuilder rootNode = documentNodeStore.getRoot().builder();
+
+        System.out.println("Starting garbage generation under the root node tmp/oak-run-generated-test-garbage");
+
+        if (CreateGarbageHelper.isEmptyProps(options.getGarbageType())) {
+            createGarbageEmptyProps(rootNode, options, generationBasePath);
+        }
+        else if (CreateGarbageHelper.isGapOrphans(options.getGarbageType())) {
+            createGarbageGapOrphans(rootNode, options, generationBasePath);
+        }
+
+        return generationBasePath;
+    }
+    private void createGarbageGapOrphans(NodeBuilder rootNode, CreateGarbageOptions options, String generationBasePath)
+            throws CommitFailedException {
+
+        // validate gap orphans depth and level gap
+        if (options.getOrphansDepth() < 0 || options.getOrphansDepth() > GAP_ORPHANS_MAX_DEPTH) {
+            LOG.error("Invalid orphansDepth specified: " + options.getOrphansDepth() + " in: " + getClass().getName()
+                    + ". Must be an integer >= 0 and <= " + GAP_ORPHANS_MAX_DEPTH);
+            System.exit(1);
+        }
+        if (options.getOrphansLevelGap() < 0 || options.getOrphansLevelGap() > options.getOrphansDepth()) {
+            LOG.error("Invalid orphansLevelGap specified: " + options.getOrphansLevelGap() + " in: " + getClass().getName()
+                    + ". Must be a positive integer <= orphansDepth");
+            System.exit(1);
+        }
+
+        System.out.println("Generating gap orphans garbage on the document: " + generationBasePath);
+
+        NodeBuilder garbageRootNode = rootNode.child(GARBAGE_GEN_ROOT_PATH_BASE).child(GARBAGE_GEN_ROOT_NODE_NAME);
+        garbageRootNode.child(generationBasePath).setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+
+        int nodesCountUnderParent = options.getCreateGarbageNodesCount() / options.getGarbageNodesParentCount();
+        for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+            // create parent node
+            garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).setProperty(JcrConstants.JCR_PRIMARYTYPE, "nt:folder", NAME);
+
+            // create child nodes under parent, according to gap orphans depth
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                getGapOrphanLeafGarbageNode(garbageRootNode, generationBasePath, options.getOrphansDepth(), i, j).
+                        setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+            }
+        }
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+
+        // Create garbage nodes - GAP_ORPHANS - remove parent nodes
+        StringBuilder sbNodePath = new StringBuilder();
+        List<String> deleteNodePaths = new ArrayList<>();
+        for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+
+            // append the parent to the paths to delete
+            sbNodePath.setLength(0);
+            String path = getIdFromPath(
+                    sbNodePath.append("/").append(GARBAGE_GEN_ROOT_PATH).append("/").append(generationBasePath).append("/")
+                            .append(GEN_PARENT_NODE_PREFIX).append(i).toString());
+            deleteNodePaths.add(path);
+
+            // append all the gap orphans nodes between the parent and the level gap to the paths to delete
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                deleteNodePaths.addAll(generateGapOrphanNodePaths(generationBasePath, i, j, options.getOrphansDepth(), options.getOrphansLevelGap(), false));
+            }
+        }
+        // Remove all parent nodes
+        documentNodeStore.getDocumentStore().remove(org.apache.jackrabbit.oak.plugins.document.Collection.NODES, deleteNodePaths);
+    }
+
+    private NodeBuilder getGapOrphanLeafGarbageNode(NodeBuilder garbageRootNode, String generationBasePath, int depth, int parentIndex, int nodeIndex) {
+        NodeBuilder garbageNode = garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + parentIndex);
+        for (int i = 1; i < depth; i++) {
+            garbageNode = garbageNode.child(GEN_NODE_PREFIX + nodeIndex + GEN_NODE_LEVEL_PREFIX + i);
+        }
+        // the leaf garbage node does not have level in its name
+        garbageNode = garbageNode.child(GEN_NODE_PREFIX + nodeIndex);
+        return garbageNode;
+    }
+
+    /**
+     * Gets the paths of the nodes between the parent node and the level gap OR between the level gap and the leaf
+     * gap orphan node.
+     *
+     * Example 1: gapDepth = 4, gapLevelGap = 2, fromLevelGapToLeaf = false will return:
+     * - /tmp/oak-run-generated-test-garbage/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1
+     * - /tmp/oak-run-generated-test-garbage/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2
+     * and will NOT return the rest of the nodes:
+     * - /tmp/oak-run-generated-test-garbage/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3
+     * - /tmp/oak-run-generated-test-garbage/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3/Node_0 (the leaf node)
+     *
+     * Example 2: gapDepth = 5, gapLevelGap = 2, fromLevelGapToLeaf = true will return:
+     * - /tmp/oak-run-generated-test-garbage/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3
+     * - /tmp/oak-run-generated-test-garbage/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3/Node_0_Level_4
+     * - /tmp/oak-run-generated-test-garbage/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2/Node_0_Level_3/Node_0_Level_4/Node_0 (the leaf node)
+     * and will NOT return the nodes from the parent to the gap level:
+     * - /tmp/oak-run-generated-test-garbage/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1
+     * - /tmp/oak-run-generated-test-garbage/GenTest_[timestamp]_0/Parent_0/Node_0_Level_1/Node_0_Level_2
+     *
+     * @param generationBasePath
+     * @param parentIndex
+     * @param nodeIndex
+     * @param gapDepth - the depth in the tree until which to create gap orphan garbage nodes.
+     * @param gapLevelGap - the gap in the tree between the first gap orphan garbage node and its parent.
+     * @param fromLevelGapToLeaf - if true, the paths will be generated from the level gap to the leaf node, otherwise the paths will be generated from the parent node to the level gap.
+     * @return
+     */
+    public static List<String> generateGapOrphanNodePaths(String generationBasePath, int parentIndex, int nodeIndex, int gapDepth, int gapLevelGap, boolean fromLevelGapToLeaf) {
+        List<String> gapOrphanNodePaths = new ArrayList<>();
+
+        StringBuilder sbNodePath = new StringBuilder();
+        sbNodePath.setLength(0);
+
+        // start path from the parent node
+        sbNodePath.append("/").append(GARBAGE_GEN_ROOT_PATH).append("/").append(generationBasePath).append("/")
+                .append(GEN_PARENT_NODE_PREFIX).append(parentIndex);
+
+        for(int i = 1; i <= gapDepth; i ++) {
+            sbNodePath.append("/").append(GEN_NODE_PREFIX).append(nodeIndex);
+            // the leaf node does NOT have level in its name
+            if (i < gapDepth) {
+                sbNodePath.append(GEN_NODE_LEVEL_PREFIX).append(i);
+            }
+
+            // only append to list if the criteria for returning the node path is met
+            if ((!fromLevelGapToLeaf && i < gapLevelGap) || (fromLevelGapToLeaf && i >= gapLevelGap)) {
+                gapOrphanNodePaths.add(getIdFromPath(sbNodePath.toString()));
+            }
+        }
+
+        return gapOrphanNodePaths;
+    }
+
+    private void createGarbageEmptyProps(NodeBuilder rootNode, CreateGarbageOptions options, String generationBasePath) throws CommitFailedException {
+
+        System.out.println("Creating empty props garbage on the document: " + generationBasePath);
+
+        NodeBuilder garbageRootNode = rootNode.child(GARBAGE_GEN_ROOT_PATH_BASE).child(GARBAGE_GEN_ROOT_NODE_NAME);
+        garbageRootNode.child(generationBasePath).setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+
+        int nodesCountUnderParent = options.getCreateGarbageNodesCount() / options.getGarbageNodesParentCount();
+        for(int i = 0; i < options.getGarbageNodesParentCount(); i ++) {
+            garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).setProperty(JcrConstants.JCR_PRIMARYTYPE, "nt:folder", NAME);
+
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                        setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, NAME);
+
+                if (CreateGarbageHelper.isEmptyProps(options.getGarbageType())) {
+                    garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                            setProperty(EMPTY_PROPERTY_NAME, "bar", STRING);
+                }
+            }
+        }
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+
+
+        //2. Create garbage nodes - EMPTY_PROPERTIES
+        for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+            for (int j = 0; j < nodesCountUnderParent; j++) {
+                garbageRootNode.child(generationBasePath).child(GEN_PARENT_NODE_PREFIX + i).child(GEN_NODE_PREFIX + j).
+                        removeProperty(EMPTY_PROPERTY_NAME);
+            }
+        }
+
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+    }
+
+    /**
+     * Cleans up all generated garbage by removing the node GARBAGE_GEN_ROOT_PATH and all of
+     * its children (recursively)
+     * @param options
+     * @param closer
+     * @throws IOException
+     * @throws Exception
+     */
+    private void cleanGarbage(CreateGarbageOptions options, Closer closer) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer).
+                setLeaseCheckMode(LeaseCheckMode.DISABLED).
+                setAsyncDelay(0).
+                setFullGCEnabled(false);
+
+        if (builder == null) {
+            System.err.println("generateGarbage mode only available for DocumentNodeStore");
+            System.exit(1);
+        }
+
+        System.out.println("Cleaning up all generated garbage: ");
+        documentNodeStore = builder.build();
+        closer.register(asCloseable(documentNodeStore));
+
+        NodeBuilder rootBuilder = documentNodeStore.getRoot().builder();
+
+        NodeBuilder generatedGarbageRootBuilder = rootBuilder.child(GARBAGE_GEN_ROOT_PATH_BASE).child(GARBAGE_GEN_ROOT_NODE_NAME);
+
+        String garbageRootNodePath = "/" + GARBAGE_GEN_ROOT_PATH;
+        List<String> childNodePaths = new ArrayList<>();
+        childNodePaths.add(getIdFromPath(garbageRootNodePath));
+
+        // get all paths of the tree nodes under the garbage root node
+        getTreeNodePaths(generatedGarbageRootBuilder, garbageRootNodePath, childNodePaths);
+
+        // check that only nodes under the garbage root node are being removed
+        for (String childNodePath : childNodePaths) {
+            assert childNodePath.substring(2).startsWith("/" + GARBAGE_GEN_ROOT_PATH);
+            System.out.println("Removing node: " + childNodePath);
+        }
+
+        documentNodeStore.getDocumentStore().remove(org.apache.jackrabbit.oak.plugins.document.Collection.NODES, childNodePaths);
+    }
+
+    /**
+     * Recursively get all paths of the tree nodes under the given root node.
+     * If there are any gaps in the tree, the paths of the gap nodes will NOT be included.
+     * @param rootNode
+     * @param basePath
+     * @param treeNodePaths
+     */
+    private void getTreeNodePaths(NodeBuilder rootNode, String basePath,
+                                  List<String> treeNodePaths) {

Review Comment:
   not sure this is a `get` method? what about a `buildTreeNodePaths`?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "shodaaan (via GitHub)" <gi...@apache.org>.
shodaaan commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1820292581


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/plugins/document/GenerateGarbageHelper.java:
##########
@@ -0,0 +1,19 @@
+package org.apache.jackrabbit.oak.plugins.document;

Review Comment:
   Fixed, thank you for the catch.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "dare-gartners (via GitHub)" <gi...@apache.org>.
dare-gartners commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1808382086


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateFullGCCommand.java:
##########
@@ -0,0 +1,416 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.plugins.document.Collection;
+import org.apache.jackrabbit.oak.plugins.document.Document;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.DocumentStoreException;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateFullGCCommand implements Command {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateFullGCCommand.class);
+
+    private static final String USAGE = "generateFullGC {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String FULLGC_GEN_ROOT_PATH = "fullGCGenRoot";
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String FULLGC_GEN_BASE_PATH = "fullGCGenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String FULLGC_GEN_PARENT_NODE_PREFIX = "fullGCParent_";
+    public static String FULLGC_GEN_NODE_PREFIX = "fullGCNode_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node FULLGC_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public GenerateFullGCOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public GenerateFullGCOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {

Review Comment:
   throwing Exception is not nice, can't you narrow it down? Also I think it's actually throwing throwable, in which case you dont need to specify throws at all



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateFullGCCommand.java:
##########
@@ -0,0 +1,416 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.plugins.document.Collection;
+import org.apache.jackrabbit.oak.plugins.document.Document;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.DocumentStoreException;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateFullGCCommand implements Command {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateFullGCCommand.class);
+
+    private static final String USAGE = "generateFullGC {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String FULLGC_GEN_ROOT_PATH = "fullGCGenRoot";
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String FULLGC_GEN_BASE_PATH = "fullGCGenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String FULLGC_GEN_PARENT_NODE_PREFIX = "fullGCParent_";
+    public static String FULLGC_GEN_NODE_PREFIX = "fullGCNode_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node FULLGC_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public GenerateFullGCOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public GenerateFullGCOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {
+        Closer closer = Closer.create();
+        try {
+            execute(closer, args);
+        } catch (Throwable e) {
+            LOG.error("Command failed", e);
+            throw closer.rethrow(e);
+        } finally {
+            closer.close();
+        }
+    }
+
+    /**
+     * Method with passed closer is necessary in order to allow for unit tests to check the output of the command.
+     * It is the responsibility of the caller to close the closer.
+     *
+     * Returns the list of generated garbage base paths (under the garbage root node).
+     * @param closer
+     * @param args
+     * @throws Exception
+     */
+    public List<String> execute(Closer closer, String... args) throws Exception {
+        continuousRunIndex = 0;
+
+        List<String> generateBasePaths = new ArrayList<>();
+
+        GenerateFullGCOptions options = new GenerateFullGCOptions(USAGE).parse(args);
+        String subCmd = options.getSubCmd();
+
+        if (GenerateFullGCOptions.CMD_GENERATE.equals(subCmd)) {
+            if (options.getNumberOfRuns() > 1 && options.getGenerateIntervalSeconds() > 0) {
+                generateBasePaths.addAll(generateGarbageContinuously(options, closer));
+            } else {
+                generateBasePaths.add(generateGarbage(options, closer, 0));
+            }
+        } else if (GenerateFullGCOptions.CMD_CLEAN.equals(subCmd)) {
+            cleanGarbage(options, closer);
+        } else {
+            System.err.println("unknown revisions command: " + subCmd);
+        }
+
+        return generateBasePaths;
+    }
+
+    private List<String> generateGarbageContinuously(GenerateFullGCOptions options, Closer closer) throws IOException, Exception {
+        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long startGenTimestamp = System.currentTimeMillis();
+
+        List<String> generatedGarbageBasePaths = new ArrayList<>();
+
+        int numberOfRuns = options.getNumberOfRuns();
+        int intervalSeconds = options.getGenerateIntervalSeconds();
+        Runnable task = () -> {
+            if (continuousRunIndex < numberOfRuns) {
+                try {
+                    String genBasePath = generateGarbage(options, closer, continuousRunIndex, builder, startGenTimestamp);
+                    generatedGarbageBasePaths.add(genBasePath);
+                } catch (Exception e) {
+                    LOG.error("Error generating garbage in run " + continuousRunIndex, e);
+                }
+                LOG.info("Task executed. Count: " + (continuousRunIndex + 1));
+                continuousRunIndex++;
+            } else {
+                // Shutdown the executor once the task has run numberOfRuns times
+                executor.shutdown();
+                LOG.info("Task completed " + numberOfRuns + " times. Stopping execution.");
+            }
+        };
+
+        // Schedule the task to run every intervalSeconds
+        executor.scheduleAtFixedRate(task, 0, intervalSeconds, TimeUnit.SECONDS);
+
+        return generatedGarbageBasePaths;
+    }
+
+    /**
+     * Generate garbage nodes in the repository in order to allow for testing fullGC functionality.
+     *
+     * Returns the path of the generated FULLGC_GEN_BASE_PATH node (under the root).
+     * @param options
+     * @param closer
+     * @param runIndex
+     * @return
+     * @throws IOException
+     * @throws Exception
+     */
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long generationTimestamp = System.currentTimeMillis();
+
+        return generateGarbage(options, closer, runIndex, builder, generationTimestamp);
+    }
+
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex,
+                                  DocumentNodeStoreBuilder<?> builder, long timestamp) throws IOException, Exception {
+
+        if (builder == null) {
+            System.err.println("generateFullGC mode only available for DocumentNodeStore");
+            System.exit(1);
+        }
+
+        String generationBasePath = FULLGC_GEN_BASE_PATH + timestamp + "_" + runIndex;
+        System.out.println("Generating fullGC on the document: " + generationBasePath);
+        documentNodeStore = builder.build();
+
+        VersionGarbageCollector.FullGCMode fullGCMode = getFullGCMode(options);
+        if (fullGCMode == VersionGarbageCollector.FullGCMode.NONE) {
+            LOG.error("Invalid garbageType specified. Must be one of the following: 1 - EMPTYPROPS, 2 - GAP_ORPHANS, 3 - GAP_ORPHANS_EMPTYPROPS");
+            System.exit(1);
+        }
+
+        //1. Create nodes with properties
+        NodeBuilder rootNode = documentNodeStore.getRoot().builder();
+        NodeBuilder garbageRootNode = rootNode.child(FULLGC_GEN_ROOT_PATH);
+        garbageRootNode.child(generationBasePath).setProperty("jcr:primaryType", "nt:file", NAME);
+
+        int nodesCountUnderParent = options.getCreateGarbageNodesCount() / options.getGarbageNodesParentCount();

Review Comment:
   can options.getGarbageNodesParentCount() be 0 ? 



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateFullGCCommand.java:
##########
@@ -0,0 +1,416 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.plugins.document.Collection;
+import org.apache.jackrabbit.oak.plugins.document.Document;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.DocumentStoreException;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateFullGCCommand implements Command {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateFullGCCommand.class);
+
+    private static final String USAGE = "generateFullGC {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String FULLGC_GEN_ROOT_PATH = "fullGCGenRoot";
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String FULLGC_GEN_BASE_PATH = "fullGCGenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String FULLGC_GEN_PARENT_NODE_PREFIX = "fullGCParent_";
+    public static String FULLGC_GEN_NODE_PREFIX = "fullGCNode_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node FULLGC_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public GenerateFullGCOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public GenerateFullGCOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {
+        Closer closer = Closer.create();
+        try {
+            execute(closer, args);
+        } catch (Throwable e) {
+            LOG.error("Command failed", e);
+            throw closer.rethrow(e);
+        } finally {
+            closer.close();
+        }
+    }
+
+    /**
+     * Method with passed closer is necessary in order to allow for unit tests to check the output of the command.
+     * It is the responsibility of the caller to close the closer.
+     *
+     * Returns the list of generated garbage base paths (under the garbage root node).
+     * @param closer
+     * @param args
+     * @throws Exception
+     */
+    public List<String> execute(Closer closer, String... args) throws Exception {
+        continuousRunIndex = 0;
+
+        List<String> generateBasePaths = new ArrayList<>();
+
+        GenerateFullGCOptions options = new GenerateFullGCOptions(USAGE).parse(args);
+        String subCmd = options.getSubCmd();
+
+        if (GenerateFullGCOptions.CMD_GENERATE.equals(subCmd)) {
+            if (options.getNumberOfRuns() > 1 && options.getGenerateIntervalSeconds() > 0) {
+                generateBasePaths.addAll(generateGarbageContinuously(options, closer));
+            } else {
+                generateBasePaths.add(generateGarbage(options, closer, 0));
+            }
+        } else if (GenerateFullGCOptions.CMD_CLEAN.equals(subCmd)) {
+            cleanGarbage(options, closer);
+        } else {
+            System.err.println("unknown revisions command: " + subCmd);
+        }
+
+        return generateBasePaths;
+    }
+
+    private List<String> generateGarbageContinuously(GenerateFullGCOptions options, Closer closer) throws IOException, Exception {

Review Comment:
   IoException is a child class of Exception, so you don't need to specify both in the throws. And again I'd suggest not throwing Exception and narrowing down to specific exceptions that the function might throw



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateFullGCCommand.java:
##########
@@ -0,0 +1,416 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.plugins.document.Collection;
+import org.apache.jackrabbit.oak.plugins.document.Document;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.DocumentStoreException;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateFullGCCommand implements Command {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateFullGCCommand.class);
+
+    private static final String USAGE = "generateFullGC {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String FULLGC_GEN_ROOT_PATH = "fullGCGenRoot";
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String FULLGC_GEN_BASE_PATH = "fullGCGenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String FULLGC_GEN_PARENT_NODE_PREFIX = "fullGCParent_";
+    public static String FULLGC_GEN_NODE_PREFIX = "fullGCNode_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node FULLGC_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public GenerateFullGCOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public GenerateFullGCOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {
+        Closer closer = Closer.create();
+        try {
+            execute(closer, args);
+        } catch (Throwable e) {
+            LOG.error("Command failed", e);
+            throw closer.rethrow(e);
+        } finally {
+            closer.close();
+        }
+    }
+
+    /**
+     * Method with passed closer is necessary in order to allow for unit tests to check the output of the command.
+     * It is the responsibility of the caller to close the closer.
+     *
+     * Returns the list of generated garbage base paths (under the garbage root node).
+     * @param closer
+     * @param args
+     * @throws Exception
+     */
+    public List<String> execute(Closer closer, String... args) throws Exception {
+        continuousRunIndex = 0;
+
+        List<String> generateBasePaths = new ArrayList<>();
+
+        GenerateFullGCOptions options = new GenerateFullGCOptions(USAGE).parse(args);
+        String subCmd = options.getSubCmd();
+
+        if (GenerateFullGCOptions.CMD_GENERATE.equals(subCmd)) {
+            if (options.getNumberOfRuns() > 1 && options.getGenerateIntervalSeconds() > 0) {
+                generateBasePaths.addAll(generateGarbageContinuously(options, closer));
+            } else {
+                generateBasePaths.add(generateGarbage(options, closer, 0));
+            }
+        } else if (GenerateFullGCOptions.CMD_CLEAN.equals(subCmd)) {
+            cleanGarbage(options, closer);
+        } else {
+            System.err.println("unknown revisions command: " + subCmd);
+        }
+
+        return generateBasePaths;
+    }
+
+    private List<String> generateGarbageContinuously(GenerateFullGCOptions options, Closer closer) throws IOException, Exception {
+        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long startGenTimestamp = System.currentTimeMillis();
+
+        List<String> generatedGarbageBasePaths = new ArrayList<>();
+
+        int numberOfRuns = options.getNumberOfRuns();
+        int intervalSeconds = options.getGenerateIntervalSeconds();
+        Runnable task = () -> {
+            if (continuousRunIndex < numberOfRuns) {
+                try {
+                    String genBasePath = generateGarbage(options, closer, continuousRunIndex, builder, startGenTimestamp);
+                    generatedGarbageBasePaths.add(genBasePath);
+                } catch (Exception e) {
+                    LOG.error("Error generating garbage in run " + continuousRunIndex, e);
+                }
+                LOG.info("Task executed. Count: " + (continuousRunIndex + 1));
+                continuousRunIndex++;
+            } else {
+                // Shutdown the executor once the task has run numberOfRuns times
+                executor.shutdown();
+                LOG.info("Task completed " + numberOfRuns + " times. Stopping execution.");
+            }
+        };
+
+        // Schedule the task to run every intervalSeconds
+        executor.scheduleAtFixedRate(task, 0, intervalSeconds, TimeUnit.SECONDS);
+
+        return generatedGarbageBasePaths;
+    }
+
+    /**
+     * Generate garbage nodes in the repository in order to allow for testing fullGC functionality.
+     *
+     * Returns the path of the generated FULLGC_GEN_BASE_PATH node (under the root).
+     * @param options
+     * @param closer
+     * @param runIndex
+     * @return
+     * @throws IOException
+     * @throws Exception
+     */
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long generationTimestamp = System.currentTimeMillis();
+
+        return generateGarbage(options, closer, runIndex, builder, generationTimestamp);
+    }
+
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex,
+                                  DocumentNodeStoreBuilder<?> builder, long timestamp) throws IOException, Exception {
+
+        if (builder == null) {
+            System.err.println("generateFullGC mode only available for DocumentNodeStore");
+            System.exit(1);
+        }
+
+        String generationBasePath = FULLGC_GEN_BASE_PATH + timestamp + "_" + runIndex;
+        System.out.println("Generating fullGC on the document: " + generationBasePath);
+        documentNodeStore = builder.build();
+
+        VersionGarbageCollector.FullGCMode fullGCMode = getFullGCMode(options);
+        if (fullGCMode == VersionGarbageCollector.FullGCMode.NONE) {
+            LOG.error("Invalid garbageType specified. Must be one of the following: 1 - EMPTYPROPS, 2 - GAP_ORPHANS, 3 - GAP_ORPHANS_EMPTYPROPS");
+            System.exit(1);
+        }
+
+        //1. Create nodes with properties
+        NodeBuilder rootNode = documentNodeStore.getRoot().builder();
+        NodeBuilder garbageRootNode = rootNode.child(FULLGC_GEN_ROOT_PATH);
+        garbageRootNode.child(generationBasePath).setProperty("jcr:primaryType", "nt:file", NAME);
+
+        int nodesCountUnderParent = options.getCreateGarbageNodesCount() / options.getGarbageNodesParentCount();
+        for(int i = 0; i < options.getGarbageNodesParentCount(); i ++) {
+            garbageRootNode.child(generationBasePath).child(FULLGC_GEN_PARENT_NODE_PREFIX + i).setProperty("jcr:primaryType", "nt:folder", NAME);
+
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                garbageRootNode.child(generationBasePath).child(FULLGC_GEN_PARENT_NODE_PREFIX + i).child(FULLGC_GEN_NODE_PREFIX + j).
+                        setProperty("jcr:primaryType", "nt:file", NAME);
+
+                if (fullGCMode == VersionGarbageCollector.FullGCMode.EMPTYPROPS || fullGCMode == VersionGarbageCollector.FullGCMode.GAP_ORPHANS_EMPTYPROPS) {
+                    garbageRootNode.child(generationBasePath).child(FULLGC_GEN_PARENT_NODE_PREFIX + i).child(FULLGC_GEN_NODE_PREFIX + j).
+                            setProperty(EMPTY_PROPERTY_NAME, "bar", STRING);
+                }
+            }
+        }
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);

Review Comment:
   these 2 operations you do again on Line 305&306 and Line 321/322. If fullGCMode can only have 1 value, wouldn't it be enough to do it just once at the end of all If:s ? 
   `documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
           documentNodeStore.runBackgroundOperations();`



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateFullGCCommand.java:
##########
@@ -0,0 +1,416 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.plugins.document.Collection;
+import org.apache.jackrabbit.oak.plugins.document.Document;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.DocumentStoreException;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateFullGCCommand implements Command {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateFullGCCommand.class);
+
+    private static final String USAGE = "generateFullGC {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String FULLGC_GEN_ROOT_PATH = "fullGCGenRoot";
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String FULLGC_GEN_BASE_PATH = "fullGCGenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String FULLGC_GEN_PARENT_NODE_PREFIX = "fullGCParent_";
+    public static String FULLGC_GEN_NODE_PREFIX = "fullGCNode_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node FULLGC_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public GenerateFullGCOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public GenerateFullGCOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {
+        Closer closer = Closer.create();
+        try {

Review Comment:
   if you use try with resources, you wouldn't need to write finally any more. this will be done automatically



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateFullGCCommand.java:
##########
@@ -0,0 +1,416 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.plugins.document.Collection;
+import org.apache.jackrabbit.oak.plugins.document.Document;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.DocumentStoreException;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateFullGCCommand implements Command {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateFullGCCommand.class);
+
+    private static final String USAGE = "generateFullGC {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String FULLGC_GEN_ROOT_PATH = "fullGCGenRoot";
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String FULLGC_GEN_BASE_PATH = "fullGCGenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String FULLGC_GEN_PARENT_NODE_PREFIX = "fullGCParent_";
+    public static String FULLGC_GEN_NODE_PREFIX = "fullGCNode_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node FULLGC_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public GenerateFullGCOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public GenerateFullGCOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {
+        Closer closer = Closer.create();
+        try {
+            execute(closer, args);
+        } catch (Throwable e) {
+            LOG.error("Command failed", e);
+            throw closer.rethrow(e);
+        } finally {
+            closer.close();
+        }
+    }
+
+    /**
+     * Method with passed closer is necessary in order to allow for unit tests to check the output of the command.
+     * It is the responsibility of the caller to close the closer.
+     *
+     * Returns the list of generated garbage base paths (under the garbage root node).
+     * @param closer
+     * @param args
+     * @throws Exception
+     */
+    public List<String> execute(Closer closer, String... args) throws Exception {
+        continuousRunIndex = 0;
+
+        List<String> generateBasePaths = new ArrayList<>();
+
+        GenerateFullGCOptions options = new GenerateFullGCOptions(USAGE).parse(args);
+        String subCmd = options.getSubCmd();
+
+        if (GenerateFullGCOptions.CMD_GENERATE.equals(subCmd)) {
+            if (options.getNumberOfRuns() > 1 && options.getGenerateIntervalSeconds() > 0) {
+                generateBasePaths.addAll(generateGarbageContinuously(options, closer));
+            } else {
+                generateBasePaths.add(generateGarbage(options, closer, 0));
+            }
+        } else if (GenerateFullGCOptions.CMD_CLEAN.equals(subCmd)) {
+            cleanGarbage(options, closer);
+        } else {
+            System.err.println("unknown revisions command: " + subCmd);
+        }
+
+        return generateBasePaths;
+    }
+
+    private List<String> generateGarbageContinuously(GenerateFullGCOptions options, Closer closer) throws IOException, Exception {
+        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long startGenTimestamp = System.currentTimeMillis();
+
+        List<String> generatedGarbageBasePaths = new ArrayList<>();
+
+        int numberOfRuns = options.getNumberOfRuns();
+        int intervalSeconds = options.getGenerateIntervalSeconds();
+        Runnable task = () -> {
+            if (continuousRunIndex < numberOfRuns) {
+                try {
+                    String genBasePath = generateGarbage(options, closer, continuousRunIndex, builder, startGenTimestamp);
+                    generatedGarbageBasePaths.add(genBasePath);
+                } catch (Exception e) {
+                    LOG.error("Error generating garbage in run " + continuousRunIndex, e);
+                }
+                LOG.info("Task executed. Count: " + (continuousRunIndex + 1));
+                continuousRunIndex++;
+            } else {
+                // Shutdown the executor once the task has run numberOfRuns times
+                executor.shutdown();
+                LOG.info("Task completed " + numberOfRuns + " times. Stopping execution.");
+            }
+        };
+
+        // Schedule the task to run every intervalSeconds
+        executor.scheduleAtFixedRate(task, 0, intervalSeconds, TimeUnit.SECONDS);
+
+        return generatedGarbageBasePaths;
+    }
+
+    /**
+     * Generate garbage nodes in the repository in order to allow for testing fullGC functionality.
+     *
+     * Returns the path of the generated FULLGC_GEN_BASE_PATH node (under the root).
+     * @param options
+     * @param closer
+     * @param runIndex
+     * @return
+     * @throws IOException
+     * @throws Exception
+     */
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long generationTimestamp = System.currentTimeMillis();
+
+        return generateGarbage(options, closer, runIndex, builder, generationTimestamp);
+    }
+
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex,
+                                  DocumentNodeStoreBuilder<?> builder, long timestamp) throws IOException, Exception {

Review Comment:
   why dont you generate/create timestamp in this method instead of passing it down? It seems to be called fro only 1 another private function that does nothing other then calling this function



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "shodaaan (via GitHub)" <gi...@apache.org>.
shodaaan commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1809012033


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateFullGCCommand.java:
##########
@@ -0,0 +1,416 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.plugins.document.Collection;
+import org.apache.jackrabbit.oak.plugins.document.Document;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.DocumentStoreException;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateFullGCCommand implements Command {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateFullGCCommand.class);
+
+    private static final String USAGE = "generateFullGC {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String FULLGC_GEN_ROOT_PATH = "fullGCGenRoot";
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String FULLGC_GEN_BASE_PATH = "fullGCGenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String FULLGC_GEN_PARENT_NODE_PREFIX = "fullGCParent_";
+    public static String FULLGC_GEN_NODE_PREFIX = "fullGCNode_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node FULLGC_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public GenerateFullGCOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public GenerateFullGCOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {
+        Closer closer = Closer.create();
+        try {
+            execute(closer, args);
+        } catch (Throwable e) {
+            LOG.error("Command failed", e);
+            throw closer.rethrow(e);
+        } finally {
+            closer.close();
+        }
+    }
+
+    /**
+     * Method with passed closer is necessary in order to allow for unit tests to check the output of the command.
+     * It is the responsibility of the caller to close the closer.
+     *
+     * Returns the list of generated garbage base paths (under the garbage root node).
+     * @param closer
+     * @param args
+     * @throws Exception
+     */
+    public List<String> execute(Closer closer, String... args) throws Exception {
+        continuousRunIndex = 0;
+
+        List<String> generateBasePaths = new ArrayList<>();
+
+        GenerateFullGCOptions options = new GenerateFullGCOptions(USAGE).parse(args);
+        String subCmd = options.getSubCmd();
+
+        if (GenerateFullGCOptions.CMD_GENERATE.equals(subCmd)) {
+            if (options.getNumberOfRuns() > 1 && options.getGenerateIntervalSeconds() > 0) {
+                generateBasePaths.addAll(generateGarbageContinuously(options, closer));
+            } else {
+                generateBasePaths.add(generateGarbage(options, closer, 0));
+            }
+        } else if (GenerateFullGCOptions.CMD_CLEAN.equals(subCmd)) {
+            cleanGarbage(options, closer);
+        } else {
+            System.err.println("unknown revisions command: " + subCmd);
+        }
+
+        return generateBasePaths;
+    }
+
+    private List<String> generateGarbageContinuously(GenerateFullGCOptions options, Closer closer) throws IOException, Exception {
+        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long startGenTimestamp = System.currentTimeMillis();
+
+        List<String> generatedGarbageBasePaths = new ArrayList<>();
+
+        int numberOfRuns = options.getNumberOfRuns();
+        int intervalSeconds = options.getGenerateIntervalSeconds();
+        Runnable task = () -> {
+            if (continuousRunIndex < numberOfRuns) {
+                try {
+                    String genBasePath = generateGarbage(options, closer, continuousRunIndex, builder, startGenTimestamp);
+                    generatedGarbageBasePaths.add(genBasePath);
+                } catch (Exception e) {
+                    LOG.error("Error generating garbage in run " + continuousRunIndex, e);
+                }
+                LOG.info("Task executed. Count: " + (continuousRunIndex + 1));
+                continuousRunIndex++;
+            } else {
+                // Shutdown the executor once the task has run numberOfRuns times
+                executor.shutdown();
+                LOG.info("Task completed " + numberOfRuns + " times. Stopping execution.");
+            }
+        };
+
+        // Schedule the task to run every intervalSeconds
+        executor.scheduleAtFixedRate(task, 0, intervalSeconds, TimeUnit.SECONDS);
+
+        return generatedGarbageBasePaths;
+    }
+
+    /**
+     * Generate garbage nodes in the repository in order to allow for testing fullGC functionality.
+     *
+     * Returns the path of the generated FULLGC_GEN_BASE_PATH node (under the root).
+     * @param options
+     * @param closer
+     * @param runIndex
+     * @return
+     * @throws IOException
+     * @throws Exception
+     */
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex) throws IOException, Exception {
+
+        DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+        long generationTimestamp = System.currentTimeMillis();
+
+        return generateGarbage(options, closer, runIndex, builder, generationTimestamp);
+    }
+
+    private String generateGarbage(GenerateFullGCOptions options, Closer closer, int runIndex,
+                                  DocumentNodeStoreBuilder<?> builder, long timestamp) throws IOException, Exception {
+
+        if (builder == null) {
+            System.err.println("generateFullGC mode only available for DocumentNodeStore");
+            System.exit(1);
+        }
+
+        String generationBasePath = FULLGC_GEN_BASE_PATH + timestamp + "_" + runIndex;
+        System.out.println("Generating fullGC on the document: " + generationBasePath);
+        documentNodeStore = builder.build();
+
+        VersionGarbageCollector.FullGCMode fullGCMode = getFullGCMode(options);
+        if (fullGCMode == VersionGarbageCollector.FullGCMode.NONE) {
+            LOG.error("Invalid garbageType specified. Must be one of the following: 1 - EMPTYPROPS, 2 - GAP_ORPHANS, 3 - GAP_ORPHANS_EMPTYPROPS");
+            System.exit(1);
+        }
+
+        //1. Create nodes with properties
+        NodeBuilder rootNode = documentNodeStore.getRoot().builder();
+        NodeBuilder garbageRootNode = rootNode.child(FULLGC_GEN_ROOT_PATH);
+        garbageRootNode.child(generationBasePath).setProperty("jcr:primaryType", "nt:file", NAME);
+
+        int nodesCountUnderParent = options.getCreateGarbageNodesCount() / options.getGarbageNodesParentCount();
+        for(int i = 0; i < options.getGarbageNodesParentCount(); i ++) {
+            garbageRootNode.child(generationBasePath).child(FULLGC_GEN_PARENT_NODE_PREFIX + i).setProperty("jcr:primaryType", "nt:folder", NAME);
+
+            for(int j = 0; j < nodesCountUnderParent; j ++) {
+                garbageRootNode.child(generationBasePath).child(FULLGC_GEN_PARENT_NODE_PREFIX + i).child(FULLGC_GEN_NODE_PREFIX + j).
+                        setProperty("jcr:primaryType", "nt:file", NAME);
+
+                if (fullGCMode == VersionGarbageCollector.FullGCMode.EMPTYPROPS || fullGCMode == VersionGarbageCollector.FullGCMode.GAP_ORPHANS_EMPTYPROPS) {
+                    garbageRootNode.child(generationBasePath).child(FULLGC_GEN_PARENT_NODE_PREFIX + i).child(FULLGC_GEN_NODE_PREFIX + j).
+                            setProperty(EMPTY_PROPERTY_NAME, "bar", STRING);
+                }
+            }
+        }
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+
+
+        //2. Generate garbage nodes - EMPTY_PROPERTIES
+        if (fullGCMode == VersionGarbageCollector.FullGCMode.EMPTYPROPS) {
+            for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+                for (int j = 0; j < nodesCountUnderParent; j++) {
+                    garbageRootNode.child(generationBasePath).child(FULLGC_GEN_PARENT_NODE_PREFIX + i).child(FULLGC_GEN_NODE_PREFIX + j).
+                            removeProperty(EMPTY_PROPERTY_NAME);
+                }
+            }
+        }
+        documentNodeStore.merge(rootNode, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        documentNodeStore.runBackgroundOperations();
+
+        //3.1. Generate garbage nodes - GAP_ORPHANS - remove parent nodes
+        if (fullGCMode == VersionGarbageCollector.FullGCMode.GAP_ORPHANS) {
+            StringBuilder sbNodePath = new StringBuilder();
+            List<String> deleteNodePaths = new ArrayList<>();
+            for (int i = 0; i < options.getGarbageNodesParentCount(); i++) {
+
+                sbNodePath.setLength(0);
+                sbNodePath.append("3:/").append(FULLGC_GEN_ROOT_PATH).append("/").append(generationBasePath).append("/").

Review Comment:
   Done. Thank you for the suggestion.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "shodaaan (via GitHub)" <gi...@apache.org>.
shodaaan commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1809013339


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateFullGCCommand.java:
##########
@@ -0,0 +1,416 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.plugins.document.Collection;
+import org.apache.jackrabbit.oak.plugins.document.Document;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.DocumentStoreException;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateFullGCCommand implements Command {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateFullGCCommand.class);
+
+    private static final String USAGE = "generateFullGC {<jdbc-uri> | <mongodb-uri>} [options]";
+
+    /**
+     * Root node for fullGC garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String FULLGC_GEN_ROOT_PATH = "fullGCGenRoot";
+
+    /**
+     * Base path for fullGC garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String FULLGC_GEN_BASE_PATH = "fullGCGenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String FULLGC_GEN_PARENT_NODE_PREFIX = "fullGCParent_";
+    public static String FULLGC_GEN_NODE_PREFIX = "fullGCNode_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class GenerateFullGCOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";
+
+        /**
+         * Sub-command for cleaning up all generated garbage.
+         * Using this will remove the root node FULLGC_GEN_ROOT_PATH and all of its children (recursively).
+         */
+        static final String CMD_CLEAN = "clean";
+
+        final OptionSpec<Integer> createGarbageNodesCount;
+        final OptionSpec<Integer> garbageNodesParentCount;
+        final OptionSpec<Integer> garbageType;
+        final OptionSpec<Integer> numberOfRuns;
+        final OptionSpec<Integer> generateIntervalSeconds;
+
+        public GenerateFullGCOptions(String usage) {
+            super(usage);
+            createGarbageNodesCount = parser
+                    .accepts("garbageNodesCount", "the total number of garbage nodes to create").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(0);
+            garbageNodesParentCount = parser
+                    .accepts("garbageNodesParentCount", "total number of parent nodes under which to create garbage nodes").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            garbageType = parser
+                    .accepts("garbageType", "garbage type to be generated - must be a value from VersionGarbageCollector.fullGCMode").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            numberOfRuns = parser
+                    .accepts("numberOfRuns", "the number of garbage generation runs to do. Only applies if greater than 1, " +
+                            "otherwise a single run will be done.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(1);
+            generateIntervalSeconds = parser
+                    .accepts("generateIntervalSeconds", "the interval at which to generate a complete garbage count from createGarbageNotesCount. " +
+                            "Applies only if numberOfRuns is greater than 1.").withRequiredArg()
+                    .ofType(Integer.class).defaultsTo(60);
+        }
+
+        public GenerateFullGCOptions parse(String[] args) {
+            super.parse(args);
+            return this;
+        }
+
+        String getSubCmd() {
+            List<String> args = getOtherArgs();
+            if (args.size() > 0) {
+                return args.get(0);
+            }
+            return CMD_GENERATE;
+        }
+
+        public int getCreateGarbageNodesCount() {
+            return createGarbageNodesCount.value(options);
+        }
+
+        public int getGarbageNodesParentCount() {
+            return garbageNodesParentCount.value(options);
+        }
+
+        public int getGarbageType() {
+            return garbageType.value(options);
+        }
+
+        public int getNumberOfRuns() {
+            return numberOfRuns.value(options);
+        }
+
+        public int getGenerateIntervalSeconds() {
+            return generateIntervalSeconds.value(options);
+        }
+    }
+
+    public void execute(String... args) throws Exception {

Review Comment:
   As @rishabhdaim said, we have to follow the Command interface. No change made.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "shodaaan (via GitHub)" <gi...@apache.org>.
shodaaan commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1812629747


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/GenerateGarbageCommand.java:
##########
@@ -0,0 +1,427 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.commons.concurrent.ExecutorCloser;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.GenerateGarbageHelper;
+import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateFullGCCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class GenerateGarbageCommand implements Command, Closeable {
+    private static final Logger LOG = LoggerFactory.getLogger(GenerateGarbageCommand.class);
+
+    private final ScheduledExecutorService continuousRunExecutor = Executors.newScheduledThreadPool(1);

Review Comment:
   Done, thank you for the suggestion.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] OAK-11182: Create and test general mechanism for creating MongoDocumentStore fullGC garbage for testing [jackrabbit-oak]

Posted by "rishabhdaim (via GitHub)" <gi...@apache.org>.
rishabhdaim commented on code in PR #1805:
URL: https://github.com/apache/jackrabbit-oak/pull/1805#discussion_r1831387856


##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/CreateGarbageCommand.java:
##########
@@ -0,0 +1,574 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.base.Joiner;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.commons.concurrent.ExecutorCloser;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.GenerateGarbageHelper;
+import org.apache.jackrabbit.oak.plugins.document.LeaseCheckMode;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.asCloseable;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateGarbageCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class CreateGarbageCommand implements Command, Closeable {
+    private static final Logger LOG = LoggerFactory.getLogger(CreateGarbageCommand.class);
+
+    private final ScheduledExecutorService continuousRunExecutor = Executors.newScheduledThreadPool(1);
+
+    private static final String USAGE = Joiner.on(System.lineSeparator()).join(
+            "create-test-garbage {<jdbc-uri> | <mongodb-uri>} <sub-command> [options] ",
+            "where sub-command is one of: ",
+            "  clean - clean up all generated garbage under the specified root node",
+            "  generate - generate garbage nodes in the repository, under the root node tmp/oak-run-generated-test-garbage. ",
+            "           Use the --garbageType (required) option to specify the type of garbage to generate",

Review Comment:
   ```suggestion
               "           Use the --garbageType (required) option to specify the type of garbage to create",
   ```



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/CreateGarbageCommand.java:
##########
@@ -0,0 +1,574 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.base.Joiner;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.commons.concurrent.ExecutorCloser;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.GenerateGarbageHelper;
+import org.apache.jackrabbit.oak.plugins.document.LeaseCheckMode;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.asCloseable;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateGarbageCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class CreateGarbageCommand implements Command, Closeable {
+    private static final Logger LOG = LoggerFactory.getLogger(CreateGarbageCommand.class);
+
+    private final ScheduledExecutorService continuousRunExecutor = Executors.newScheduledThreadPool(1);
+
+    private static final String USAGE = Joiner.on(System.lineSeparator()).join(
+            "create-test-garbage {<jdbc-uri> | <mongodb-uri>} <sub-command> [options] ",
+            "where sub-command is one of: ",
+            "  clean - clean up all generated garbage under the specified root node",
+            "  generate - generate garbage nodes in the repository, under the root node tmp/oak-run-generated-test-garbage. ",

Review Comment:
   ```suggestion
               "  create - create garbage nodes in the repository, under the root node tmp/oak-run-created-test-garbage. ",
   ```



##########
oak-run/src/main/java/org/apache/jackrabbit/oak/run/CreateGarbageCommand.java:
##########
@@ -0,0 +1,574 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.guava.common.base.Joiner;
+import org.apache.jackrabbit.guava.common.io.Closer;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.commons.concurrent.ExecutorCloser;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.GenerateGarbageHelper;
+import org.apache.jackrabbit.oak.plugins.document.LeaseCheckMode;
+import org.apache.jackrabbit.oak.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.apache.jackrabbit.oak.run.Utils.asCloseable;
+import static org.apache.jackrabbit.oak.run.Utils.createDocumentMKBuilder;
+
+/**
+ * GenerateGarbageCommand generates garbage nodes in the repository in order to allow for testing fullGC functionality.
+ */
+public class CreateGarbageCommand implements Command, Closeable {
+    private static final Logger LOG = LoggerFactory.getLogger(CreateGarbageCommand.class);
+
+    private final ScheduledExecutorService continuousRunExecutor = Executors.newScheduledThreadPool(1);
+
+    private static final String USAGE = Joiner.on(System.lineSeparator()).join(
+            "create-test-garbage {<jdbc-uri> | <mongodb-uri>} <sub-command> [options] ",
+            "where sub-command is one of: ",
+            "  clean - clean up all generated garbage under the specified root node",
+            "  generate - generate garbage nodes in the repository, under the root node tmp/oak-run-generated-test-garbage. ",
+            "           Use the --garbageType (required) option to specify the type of garbage to generate",
+            "           the --garbageNodesCount option to specify the total number of garbage nodes to create,",
+            "           the --garbageNodesParentCount option to specify the total number of parent nodes under which to create garbage nodes,",
+            "           the --orphansDepth option to specify the depth in the tree at which to create garbage nodes,",
+            "           the --orphansLevelGap option to specify the gap in the tree between the first leaf gap orphan garbage node and its parent,",
+            "           the --numberOfRuns option to specify the number of garbage generation runs to do,",
+            "           and the --generateIntervalSeconds option to specify the interval at which to generate a complete garbage count from createGarbageNotesCount.");
+
+
+    /**
+     * Garbage should be generated under tmp node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH_BASE = "tmp";
+
+    /**
+     * The root node name for garbage generation, one level under tmp.
+     */
+    public static String GARBAGE_GEN_ROOT_NODE_NAME = "oak-run-generated-test-garbage";
+
+    /**
+     * Root node for garbage generation.
+     * Necessary in order to allow cleanup of all generated garbage nodes by simply removing the root node.
+     */
+    public static String GARBAGE_GEN_ROOT_PATH = GARBAGE_GEN_ROOT_PATH_BASE + "/" + GARBAGE_GEN_ROOT_NODE_NAME;
+
+    /**
+     * Base path for garbage generation. The timestamp of the run will be appended to this path,
+     * which is necessary in order for each garbage generation run to be unique and not overwrite previous ones.
+     * If continuous generation is enabled, the index of the run will also be appended to this path.
+     */
+    public static String GEN_BASE_PATH = "GenTest_";
+
+    /**
+     * Prefix for parent nodes under which garbage nodes will be created.
+     * The index of the parent node will be appended to this prefix.
+     */
+    public static String GEN_PARENT_NODE_PREFIX = "Parent_";
+    public static String GEN_NODE_PREFIX = "Node_";
+    public static String GEN_NODE_LEVEL_PREFIX = "_Level_";
+
+    public static String EMPTY_PROPERTY_NAME = "prop";
+
+    /**
+     * The maximum depth in the tree at which to generate gap orphans garbage nodes.
+     * Used for validation of the orphansDepth / orphansLevelGap options.
+     */
+    private final int GAP_ORPHANS_MAX_DEPTH = 15;
+
+    private int continuousRunIndex = 0;
+
+    private DocumentNodeStore documentNodeStore;
+
+    public DocumentNodeStore getDocumentNodeStore() {
+        return documentNodeStore;
+    }
+
+    private static class CreateGarbageOptions extends Utils.NodeStoreOptions {
+
+        /**
+         * Sub-command for generating garbage.
+         * This is the default sub-command to run if none is specified.
+         */
+        static final String CMD_GENERATE = "generate";

Review Comment:
   ```suggestion
           static final String CMD_CREATE = "create";
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: oak-dev-unsubscribe@jackrabbit.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org