This is an automated email from the ASF dual-hosted git repository.

stefanegli pushed a commit to branch DetailedGC/OAK-10199
in repository https://gitbox.apache.org/repos/asf/jackrabbit-oak.git


The following commit(s) were added to refs/heads/DetailedGC/OAK-10199 by this 
push:
     new c710cd698b OAK-10743 : gap orphan mode introduced - gc modes added, 
refined and … (#1404)
c710cd698b is described below

commit c710cd698b10bc50193a18d6feca078193d5a170
Author: stefan-egli <[email protected]>
AuthorDate: Thu Apr 4 19:56:58 2024 +0200

    OAK-10743 : gap orphan mode introduced - gc modes added, refined and … 
(#1404)
    
    * OAK-10743 : gap orphan mode introduced - gc modes added, refined and 
renamed
    
    * OAK-10743 : fix compilation error - testRecentRevisionsArePreserved fails 
now however
---
 .../document/NodeDocumentRevisionCleaner.java      |  33 +-
 .../plugins/document/VersionGarbageCollector.java  | 146 ++++-
 .../oak/plugins/document/BranchCommitGCTest.java   | 259 ++++++---
 .../oak/plugins/document/DetailGCHelper.java       |   9 +-
 .../document/VersionGarbageCollectorIT.java        | 603 ++++++++++++++++-----
 5 files changed, 804 insertions(+), 246 deletions(-)

diff --git 
a/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/NodeDocumentRevisionCleaner.java
 
b/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/NodeDocumentRevisionCleaner.java
index eb6d604d41..59d32fbe4e 100644
--- 
a/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/NodeDocumentRevisionCleaner.java
+++ 
b/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/NodeDocumentRevisionCleaner.java
@@ -18,7 +18,11 @@
  */
 package org.apache.jackrabbit.oak.plugins.document;
 
+import org.apache.jackrabbit.oak.plugins.document.UpdateOp.Key;
+import org.apache.jackrabbit.oak.plugins.document.UpdateOp.Operation;
+import org.apache.jackrabbit.oak.plugins.document.UpdateOp.Operation.Type;
 import org.apache.jackrabbit.oak.plugins.document.util.Utils;
+import org.jetbrains.annotations.NotNull;
 
 import java.time.Instant;
 import java.time.temporal.ChronoUnit;
@@ -26,6 +30,7 @@ import java.util.Map;
 import java.util.SortedMap;
 import java.util.TreeMap;
 import java.util.TreeSet;
+import java.util.Map.Entry;
 
 /**
  * This is a prototype class of a very fine-grained revision cleaner that 
cleans even revisions
@@ -40,6 +45,7 @@ public class NodeDocumentRevisionCleaner {
     private final NodeDocument workingDocument;
     private final RevisionPropertiesClassifier revisionClassifier;
     private final RevisionCleanerUtility revisionCleaner;
+    private long toModifiedMs;
 
     /**
      * Constructor for NodeDocumentRevisionCleaner.
@@ -47,8 +53,18 @@ public class NodeDocumentRevisionCleaner {
      * @param workingDocument The document to clean up.
      */
     public NodeDocumentRevisionCleaner(DocumentNodeStore documentNodeStore, 
NodeDocument workingDocument) {
+        this(documentNodeStore, workingDocument, Instant.now().minus(24, 
ChronoUnit.HOURS).toEpochMilli());
+    }
+
+    /**
+     * Constructor for NodeDocumentRevisionCleaner.
+     * @param documentNodeStore The DocumentNodeStore instance.
+     * @param workingDocument The document to clean up.
+     */
+    public NodeDocumentRevisionCleaner(DocumentNodeStore documentNodeStore, 
NodeDocument workingDocument, long toModifiedMs) {
         this.workingDocument = workingDocument;
         this.documentNodeStore = documentNodeStore;
+        this.toModifiedMs = toModifiedMs;
 
         revisionClassifier = new RevisionPropertiesClassifier(workingDocument);
         revisionCleaner = new RevisionCleanerUtility(revisionClassifier);
@@ -68,11 +84,22 @@ public class NodeDocumentRevisionCleaner {
             for (Revision revision : entry.getValue()) {
                 TreeSet<String> properties = 
revisionClassifier.getPropertiesModifiedByRevision().get(revision);
                 if (properties != null) {
-                    for (String property : properties) {
+                    outer:for (String property : properties) {
+                        Map<Key, Operation> c = op.getChanges();
+                        for (Entry<Key, Operation> e : c.entrySet()) {
+                            if (e.getKey().equals(new Key(property, null)) && 
e.getValue().type == Type.REMOVE) {
+                                continue outer;
+                            }
+                        }
                         op.removeMapEntry(property, revision);
                     }
                 }
-                op.removeMapEntry("_revisions", revision);
+                RevisionVector sweepRevisions = 
documentNodeStore.getSweepRevisions();
+                boolean newerThanSweep = sweepRevisions == null ? false : 
sweepRevisions.isRevisionNewer(revision);
+                boolean isBC = 
workingDocument.getLocalBranchCommits().contains(revision);
+                if (!newerThanSweep && !isBC) {
+                    op.removeMapEntry("_revisions", revision);
+                }
             }
         }
     }
@@ -176,7 +203,7 @@ public class NodeDocumentRevisionCleaner {
         }
 
         private void preserveRevisionsNewerThanThreshold(long amount, 
ChronoUnit unit) {
-            long thresholdToPreserve = Instant.now().minus(amount, 
unit).toEpochMilli();
+            long thresholdToPreserve = 
toModifiedMs;//Instant.now().minus(amount, unit).toEpochMilli();
             for (TreeSet<Revision> revisionSet : 
candidateRevisionsToClean.values()) {
                 for (Revision revision : revisionSet) {
                     if (revision.getTimestamp() > thresholdToPreserve) {
diff --git 
a/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/VersionGarbageCollector.java
 
b/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/VersionGarbageCollector.java
index 8aece8136d..18db77b122 100644
--- 
a/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/VersionGarbageCollector.java
+++ 
b/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/VersionGarbageCollector.java
@@ -38,6 +38,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.BiConsumer;
 import java.util.function.Consumer;
+import java.util.stream.Stream;
 
 import org.apache.jackrabbit.guava.common.base.Function;
 import org.apache.jackrabbit.guava.common.base.Joiner;
@@ -150,17 +151,50 @@ public class VersionGarbageCollector {
      */
     static final String 
SETTINGS_COLLECTION_DETAILED_GC_DRY_RUN_DOCUMENT_ID_PROP = "detailedGCDryRunId";
 
-    enum RDGCType {
-        NO_OLD_PROP_REV_GC,
-        KEEP_ONE_FULL_MODE,
-        KEEP_ONE_CLEANUP_USER_PROPERTIES_ONLY_MODE,
-        OLDER_THAN_24H_AND_BETWEEN_CHECKPOINTS_MODE
+    /**
+     * During hardening of DetailedGC one can choose level type of garbage 
should be cleaned up.
+     * Ultimately the goal is to clean up all possible garbage. After 
hardening these modes
+     * might no longer be supported.
+     */
+    enum DetailedGCMode {
+        /** no detailed GC is done at all */
+        NONE,
+        /** GC only orphaned nodes with gaps in ancestor docs */
+        GAP_ORPHANS,
+        /** GC orphaned nodes with gaps in ancestor docs, plus empty 
properties */
+        GAP_ORPHANS_EMPTYPROPS,
+        /** GC any kind of orphaned nodes, plus empty properties */
+        ALL_ORPHANS_EMPTYPROPS,
+        /**
+         * GC any kind of orphaned nodes, empty properties plus keep 1 (== keep
+         * traversed) revision, applied to user properties only
+         */
+        ORPHANS_EMPTYPROPS_KEEP_ONE_USER_PROPS,
+        /**
+         * GC any kind of orphaned nodes, empty properties plus keep 1 (== keep
+         * traversed) revision, applied to all properties
+         */
+        ORPHANS_EMPTYPROPS_KEEP_ONE_ALL_PROPS,
+        /**
+         * GC any kind of orphaned nodes, empty properties plus cleanup 
unmerged BCs
+         */
+        ORPHANS_EMPTYPROPS_UNMERGED_BC,
+        /**
+         * GC any kind of orphaned nodes, empty properties plus cleanup 
revisions, also
+         * between checkpoints
+         */
+        ORPHANS_EMPTYPROPS_BETWEEN_CHECKPOINTS_NO_UNMERGED_BC,
+        /**
+         * GC any kind of orphaned nodes, empty properties, cleanup revisions, 
also
+         * between checkpoints, plus cleanup unmerged BCs
+         */
+        ORPHANS_EMPTYPROPS_BETWEEN_CHECKPOINTS_WITH_UNMERGED_BC
     }
 
-    private static RDGCType revisionDetailedGcType = 
RDGCType.NO_OLD_PROP_REV_GC;
+    private static DetailedGCMode detailedGcMode = 
DetailedGCMode.GAP_ORPHANS_EMPTYPROPS;
 
-    static RDGCType getRevisionDetailedGcType() {
-        return revisionDetailedGcType;
+    static DetailedGCMode getDetailedGcMode() {
+        return detailedGcMode;
     }
 
     private final DocumentNodeStore nodeStore;
@@ -187,7 +221,7 @@ public class VersionGarbageCollector {
         this.isDetailedGCDryRun = isDetailedGCDryRun;
         this.embeddedVerification = embeddedVerification;
         this.options = new VersionGCOptions();
-        log.info("<init> VersionGarbageCollector created with 
revisionDetailedGcType = {}", revisionDetailedGcType);
+        log.info("<init> VersionGarbageCollector created with 
revisionDetailedGcType = {}", detailedGcMode);
     }
 
     void setStatisticsProvider(StatisticsProvider provider) {
@@ -1003,6 +1037,10 @@ public class VersionGarbageCollector {
         }
 
         public void collectGarbage(final NodeDocument doc, final GCPhases 
phases) {
+            if (detailedGcMode == DetailedGCMode.NONE) {
+                // TODO we should avoid getting here in the first case then 
probably
+                return;
+            }
 
             detailedGCStats.documentRead();
             monitor.info("Collecting Detailed Garbage for doc [{}]", 
doc.getId());
@@ -1017,25 +1055,51 @@ public class VersionGarbageCollector {
 
             if (!isDeletedOrOrphanedNode(traversedState, phases, doc)) {
                 // here the node is not orphaned which means that we can reach 
the node from root
-                collectDeletedProperties(doc, phases, op, traversedState);
-                switch(revisionDetailedGcType) {
-                    case NO_OLD_PROP_REV_GC : {
+                switch(detailedGcMode) {
+                    case NONE : {
+                        // shouldn't be reached
+                        return;
+                    }
+                    case GAP_ORPHANS : {
+                        // this mode does neither unusedproprev, nor unmergedBC
+                        break;
+                    }
+                    case GAP_ORPHANS_EMPTYPROPS : {
+                        collectDeletedProperties(doc, phases, op, 
traversedState);
                         // this mode does neither unusedproprev, nor unmergedBC
                         break;
                     }
-                    case KEEP_ONE_FULL_MODE : {
+                    case ALL_ORPHANS_EMPTYPROPS : {
+                        collectDeletedProperties(doc, phases, op, 
traversedState);
+                        // this mode does neither unusedproprev, nor unmergedBC
+                        break;
+                    }
+                    case ORPHANS_EMPTYPROPS_KEEP_ONE_ALL_PROPS : {
+                        collectDeletedProperties(doc, phases, op, 
traversedState);
                         collectUnusedPropertyRevisions(doc, phases, op, 
(DocumentNodeState) traversedState, false);
                         combineInternalPropRemovals(doc, op);
                         break;
                     }
-                    case KEEP_ONE_CLEANUP_USER_PROPERTIES_ONLY_MODE : {
+                    case ORPHANS_EMPTYPROPS_KEEP_ONE_USER_PROPS : {
+                        collectDeletedProperties(doc, phases, op, 
traversedState);
                         collectUnusedPropertyRevisions(doc, phases, op, 
(DocumentNodeState) traversedState, true);
                         combineInternalPropRemovals(doc, op);
                         break;
                     }
-                    case OLDER_THAN_24H_AND_BETWEEN_CHECKPOINTS_MODE : {
+                    case ORPHANS_EMPTYPROPS_UNMERGED_BC : {
+                        collectDeletedProperties(doc, phases, op, 
traversedState);
+                        collectUnmergedBranchCommits(doc, phases, op, 
toModifiedMs);
+                        break;
+                    }
+                    case 
ORPHANS_EMPTYPROPS_BETWEEN_CHECKPOINTS_WITH_UNMERGED_BC : {
+                        collectDeletedProperties(doc, phases, op, 
traversedState);
                         collectUnmergedBranchCommits(doc, phases, op, 
toModifiedMs);
-                        collectRevisionsOlderThan24hAndBetweenCheckpoints(doc, 
phases, op);
+                        collectRevisionsOlderThan24hAndBetweenCheckpoints(doc, 
toModifiedMs, phases, op);
+                        break;
+                    }
+                    case ORPHANS_EMPTYPROPS_BETWEEN_CHECKPOINTS_NO_UNMERGED_BC 
: {
+                        collectDeletedProperties(doc, phases, op, 
traversedState);
+                        collectRevisionsOlderThan24hAndBetweenCheckpoints(doc, 
toModifiedMs, phases, op);
                         break;
                     }
                 }
@@ -1122,6 +1186,29 @@ public class VersionGarbageCollector {
                 phases.stop(GCPhase.DETAILED_GC_COLLECT_ORPHAN_NODES);
                 return false;
             }
+            if (detailedGcMode == DetailedGCMode.GAP_ORPHANS
+                    || detailedGcMode == 
DetailedGCMode.GAP_ORPHANS_EMPTYPROPS) {
+                // check the ancestor docs for gaps
+                boolean hasGaps = false;
+                boolean parentDocExists = true;
+                Path path = Path.ROOT;
+                for (String name : doc.getPath().elements()) {
+                    if (!parentDocExists) {
+                        hasGaps = true;
+                        break;
+                    }
+                    path = new Path(path, name);
+                    final NodeDocument d = 
nodeStore.getDocumentStore().find(NODES,
+                            Utils.getIdFromPath(path));
+                    parentDocExists = d != null;
+                }
+                if (!hasGaps) {
+                    // then it is not a gap orphan
+                    // nothing to do here then
+                    phases.stop(GCPhase.DETAILED_GC_COLLECT_ORPHAN_NODES);
+                    return false;
+                }
+            }
 
             // if this is an orphaned node, all that is needed is its removal
             garbageDocsCount++;
@@ -1221,7 +1308,13 @@ public class VersionGarbageCollector {
                         .filter(e -> filterEmptyProps(doc, e.getKey(), 
e.getValue()))
                         .mapToInt(e -> {
                             final String prop = e.getKey();
+                            final int origCount = updateOp.getChanges().size();
                             updateOp.getChanges().entrySet().removeIf(opEntry 
-> Objects.equals(prop, opEntry.getKey().getName()));
+                            final int diff = origCount - 
updateOp.getChanges().size();
+                            //TODO: beautify once this is in DetailedGC branch
+                            if (diff > 0) {
+                                
deletedInternalPropRevsCountMap.merge(doc.getId(), -diff, Integer::sum);
+                            }
                             updateOp.remove(prop);
                             return 1;})
                         .sum();
@@ -1385,15 +1478,32 @@ public class VersionGarbageCollector {
 
         }
 
-        private void collectRevisionsOlderThan24hAndBetweenCheckpoints(final 
NodeDocument doc,
+        private void collectRevisionsOlderThan24hAndBetweenCheckpoints(final 
NodeDocument doc, final long toModifiedMs,
                                                               final GCPhases 
phases, final UpdateOp updateOp) {
             if (phases.start(GCPhase.DETAILED_GC_COLLECT_OLD_REVS)){
-                NodeDocumentRevisionCleaner cleaner = new 
NodeDocumentRevisionCleaner(nodeStore, doc);
+                NodeDocumentRevisionCleaner cleaner = new 
NodeDocumentRevisionCleaner(nodeStore, doc, toModifiedMs);
+                final int beforeRevs = countRevs(updateOp, false);
+                final int beforeIntRevs = countRevs(updateOp, true);
                 cleaner.collectOldRevisions(updateOp);
+                final int revsDiff = countRevs(updateOp, false) - beforeRevs;
+                if (revsDiff > 0) {
+                    deletedPropRevsCountMap.merge(doc.getId(), revsDiff, 
Integer::sum);
+                }
+                final int intRevsDiff = countRevs(updateOp, true) - 
beforeIntRevs;
+                if (intRevsDiff > 0) {
+                    deletedInternalPropRevsCountMap.merge(doc.getId(), 
intRevsDiff, Integer::sum);
+                }
                 phases.stop(GCPhase.DETAILED_GC_COLLECT_OLD_REVS);
             }
         }
 
+        private int countRevs(UpdateOp updateOp, boolean internalProps) {
+            return stream(updateOp.getChanges().entrySet().spliterator(), 
false)
+                    .filter(e -> (e.getValue().type == Type.REMOVE_MAP_ENTRY))
+                    .filter(e -> (e.getKey().getName().startsWith("_") == 
internalProps))
+                    .mapToInt(x -> 1).sum();
+        }
+
         /**
          * Remove all property revisions in the local document that are no 
longer used.
          * This includes bundled properties. It also includes related entries 
that
diff --git 
a/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/BranchCommitGCTest.java
 
b/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/BranchCommitGCTest.java
index 36d58d589e..5ae5bf83c1 100644
--- 
a/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/BranchCommitGCTest.java
+++ 
b/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/BranchCommitGCTest.java
@@ -16,8 +16,40 @@
  */
 package org.apache.jackrabbit.oak.plugins.document;
 
+import static java.util.concurrent.TimeUnit.HOURS;
+import static org.apache.commons.lang3.reflect.FieldUtils.writeField;
+import static org.apache.commons.lang3.reflect.FieldUtils.writeStaticField;
+import static 
org.apache.jackrabbit.oak.plugins.document.DetailGCHelper.assertBranchRevisionRemovedFromAllDocuments;
+import static org.apache.jackrabbit.oak.plugins.document.DetailGCHelper.build;
+import static 
org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollectorIT.allOrphProp;
+import static 
org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollectorIT.assertStatsCountsEqual;
+import static 
org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollectorIT.assertStatsCountsZero;
+import static 
org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollectorIT.betweenChkp;
+import static 
org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollectorIT.btwnChkpUBC;
+import static 
org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollectorIT.gapOrphOnly;
+import static 
org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollectorIT.gapOrphProp;
+import static 
org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollectorIT.isModeOneOf;
+import static 
org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollectorIT.keepOneFull;
+import static 
org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollectorIT.keepOneUser;
+import static 
org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollectorIT.unmergedBcs;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Objects;
+import java.util.SortedMap;
+import java.util.function.Consumer;
+
 import org.apache.jackrabbit.oak.plugins.document.DocumentMK.Builder;
-import 
org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector.RDGCType;
+import 
org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector.DetailedGCMode;
+import 
org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollectorIT.GCCounts;
 import org.apache.jackrabbit.oak.plugins.document.util.Utils;
 import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
 import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
@@ -31,32 +63,6 @@ import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 import org.junit.runners.Parameterized.Parameter;
 
-import java.io.IOException;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Objects;
-import java.util.SortedMap;
-import java.util.function.Consumer;
-
-import static java.util.concurrent.TimeUnit.HOURS;
-import static org.apache.commons.lang3.reflect.FieldUtils.writeField;
-import static org.apache.commons.lang3.reflect.FieldUtils.writeStaticField;
-import static 
org.apache.jackrabbit.oak.plugins.document.DetailGCHelper.assertBranchRevisionRemovedFromAllDocuments;
-import static org.apache.jackrabbit.oak.plugins.document.DetailGCHelper.build;
-import static 
org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollectorIT.noOldPropGc;
-import static 
org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollectorIT.keepOneFull;
-import static 
org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollectorIT.keepOneUser;
-import static 
org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollectorIT.betweenChkp;
-import static 
org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollectorIT.assertStatsCountsEqual;
-import static 
org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollectorIT.assertStatsCountsZero;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-
 @RunWith(Parameterized.class)
 public class BranchCommitGCTest {
 
@@ -72,7 +78,7 @@ public class BranchCommitGCTest {
         java.util.Collection<Object[]> params = new LinkedList<>();
         for (Object[] fixture : AbstractDocumentStoreTest.fixtures()) {
             DocumentStoreFixture f = (DocumentStoreFixture)fixture[0];
-            for (RDGCType gcType : RDGCType.values()) {
+            for (DetailedGCMode gcType : DetailedGCMode.values()) {
                 params.add(new Object[] {f, gcType});
             }
         }
@@ -83,9 +89,9 @@ public class BranchCommitGCTest {
     public DocumentStoreFixture fixture;
 
     @Parameter(1)
-    public RDGCType gcType;
+    public DetailedGCMode detailedGcMode;
 
-    private RDGCType originalGcType;
+    private DetailedGCMode originalDetailedGcMode;
 
     @Before
     public void setUp() throws Exception {
@@ -96,13 +102,13 @@ public class BranchCommitGCTest {
                 
.setLeaseCheckMode(LeaseCheckMode.DISABLED).setDetailedGCEnabled(true)
                 .setAsyncDelay(0).getNodeStore();
         gc = store.getVersionGarbageCollector();
-        originalGcType = VersionGarbageCollector.getRevisionDetailedGcType();
-        writeStaticField(VersionGarbageCollector.class, 
"revisionDetailedGcType", gcType, true);
+        originalDetailedGcMode = VersionGarbageCollector.getDetailedGcMode();
+        writeStaticField(VersionGarbageCollector.class, "detailedGcMode", 
detailedGcMode, true);
     }
 
     @After
     public void tearDown() throws Exception {
-        writeStaticField(VersionGarbageCollector.class, 
"revisionDetailedGcType", originalGcType, true);
+        writeStaticField(VersionGarbageCollector.class, "detailedGcMode", 
originalDetailedGcMode, true);
         assertNoEmptyProperties();
         if (store != null) {
             store.dispose();
@@ -129,18 +135,24 @@ public class BranchCommitGCTest {
         VersionGarbageCollector.VersionGCStats stats = gc.gc(1, HOURS);
 
         assertStatsCountsEqual(stats,
-                noOldPropGc(2, 0, 0, 0, 0, 0, 2),
+                gapOrphOnly(),
+                gapOrphProp(),
+                allOrphProp(2, 0, 0, 0, 0, 0, 2),
                 keepOneFull(2, 0, 1, 0, 1, 0, 3),
                 keepOneUser(2, 0, 0, 0, 0, 0, 2),
-                betweenChkp(2, 0, 1, 0, 2, 1, 3));
+                betweenChkp(2, 0, 0, 0, 0, 0, 2),
+                unmergedBcs(2, 0, 1, 0, 1, 1, 3),
+                btwnChkpUBC(2, 0, 1, 0, 1, 1, 3));
 
         // now do another gc - should not have anything left to clean up though
         clock.waitUntil(clock.getTime() + HOURS.toMillis(2));
         stats = gc.gc(1, HOURS);
         assertStatsCountsZero(stats);
-        assertNotExists("1:/a");
-        assertNotExists("1:/b");
-        assertBranchRevisionRemovedFromAllDocuments(store, br);
+        if (!isModeOneOf(DetailedGCMode.NONE, DetailedGCMode.GAP_ORPHANS, 
DetailedGCMode.GAP_ORPHANS_EMPTYPROPS)) {
+            assertNotExists("1:/a");
+            assertNotExists("1:/b");
+            assertBranchRevisionRemovedFromAllDocuments(store, br);
+        }
     }
 
     @Test
@@ -182,17 +194,29 @@ public class BranchCommitGCTest {
             // expects a collision to have happened - which was cleaned up - 
hence a _bc (but not the _revision I guess)
             // the collisions cleaned up - with the 1 collision and a _bc
             assertStatsCountsEqual(stats,
-                    noOldPropGc(2, 0, 0, 0, 0, 0, 0),
+                    new GCCounts(DetailedGCMode.NONE, 
+                                2, 0, 0, 0, 0, 0, 0),
+                    gapOrphOnly(2, 0, 0, 0, 0, 0, 0),
+                    gapOrphProp(2, 0, 0, 0, 0, 0, 0),
+                    allOrphProp(2, 0, 0, 0, 0, 0, 0),
                     keepOneFull(2, 0, 1, 0, 2, 0, 1),
                     keepOneUser(2, 0, 0, 0, 0, 0, 0),
-                    betweenChkp(2, 0, 1, 0, 3, 1, 1));
+                    betweenChkp(2, 0, 0, 0, 0, 0, 0),
+                    unmergedBcs(2, 0, 1, 0, 2, 1, 1),
+                    btwnChkpUBC(2, 0, 1, 0, 2, 1, 1));
         } else {
             // in this case classic collision cleanup already took care of 
everything, nothing left
             assertStatsCountsEqual(stats,
-                    noOldPropGc(2, 0, 0, 0, 0, 0, 0),
+                    new GCCounts(DetailedGCMode.NONE, 
+                                2, 0, 0, 0, 0, 0, 0),
+                    gapOrphOnly(2, 0, 0, 0, 0, 0, 0),
+                    gapOrphProp(2, 0, 0, 0, 0, 0, 0),
+                    allOrphProp(2, 0, 0, 0, 0, 0, 0),
                     keepOneFull(2, 1, 0, 0, 0, 0, 0),
                     keepOneUser(2, 1, 0, 0, 0, 0, 0),
-                    betweenChkp(2, 1, 0, 0, 0, 0, 0));
+                    betweenChkp(2, 0, 0, 0, 0, 0, 0),
+                    unmergedBcs(2, 0, 0, 0, 0, 0, 0),
+                    btwnChkpUBC(2, 0, 0, 0, 0, 0, 0));
         }
 
         assertNotExists("1:/a");
@@ -247,14 +271,20 @@ public class BranchCommitGCTest {
 
         // 6 deleted props: 0:/[_collisions], 1:/foo[p, a], 
1:/bar[_bc,prop,_revisions]
         assertStatsCountsEqual(stats,
-                noOldPropGc(0, 3, 0, 0, 0, 0, 2),
+                gapOrphOnly(),
+                gapOrphProp(0, 3, 0, 0, 0, 0, 2),
+                allOrphProp(0, 3, 0, 0, 0, 0, 2),
                 keepOneFull(0, 3, 2, 1,17, 0, 3),
                 keepOneUser(0, 3, 0, 1, 0, 0, 2),
-                betweenChkp(0, 3, 2, 1,18, 4, 3));
-        assertBranchRevisionRemovedFromAllDocuments(store, br1);
-        assertBranchRevisionRemovedFromAllDocuments(store, br2);
-        assertBranchRevisionRemovedFromAllDocuments(store, br3);
-        assertBranchRevisionRemovedFromAllDocuments(store, br4);
+                betweenChkp(0, 3, 0, 0, 1, 0, 3),
+                unmergedBcs(0, 3, 2, 1,15, 4, 3),
+                btwnChkpUBC(0, 3, 2, 1,16, 4, 3));
+        if (!isModeOneOf(DetailedGCMode.NONE, DetailedGCMode.GAP_ORPHANS)) {
+            assertBranchRevisionRemovedFromAllDocuments(store, br1);
+            assertBranchRevisionRemovedFromAllDocuments(store, br2);
+            assertBranchRevisionRemovedFromAllDocuments(store, br3);
+            assertBranchRevisionRemovedFromAllDocuments(store, br4);
+        }
     }
 
     @Test
@@ -280,20 +310,28 @@ public class BranchCommitGCTest {
         VersionGarbageCollector.VersionGCStats stats = gc.gc(1, HOURS);
 
         assertStatsCountsEqual(stats,
-                noOldPropGc(2, 0, 0, 0, 0, 0, 2),
+                gapOrphOnly(),
+                gapOrphProp(),
+                allOrphProp(2, 0, 0, 0, 0, 0, 2),
                 keepOneFull(2, 0, 2, 0, 2, 0, 3),
                 keepOneUser(2, 0, 0, 0, 0, 0, 2),
-                betweenChkp(2, 0, 2, 0, 5, 2, 3));
+                betweenChkp(2, 0, 0, 0, 0, 0, 2),
+                unmergedBcs(2, 0, 2, 0, 2, 2, 3),
+                btwnChkpUBC(2, 0, 2, 0, 2, 2, 3));
 
-        assertNotExists("1:/a");
-        assertNotExists("1:/b");
+        if (!isModeOneOf(DetailedGCMode.NONE, DetailedGCMode.GAP_ORPHANS, 
DetailedGCMode.GAP_ORPHANS_EMPTYPROPS)) {
+            assertNotExists("1:/a");
+            assertNotExists("1:/b");
+        }
 
         // now do another gc - should not have anything left to clean up though
         clock.waitUntil(clock.getTime() + HOURS.toMillis(2));
         stats = gc.gc(1, HOURS);
         assertStatsCountsZero(stats);
-        assertBranchRevisionRemovedFromAllDocuments(store, br1);
-        assertBranchRevisionRemovedFromAllDocuments(store, br2);
+        if (!isModeOneOf(DetailedGCMode.NONE)) {
+            assertBranchRevisionRemovedFromAllDocuments(store, br1);
+            assertBranchRevisionRemovedFromAllDocuments(store, br2);
+        }
     }
 
     @Test
@@ -326,10 +364,14 @@ public class BranchCommitGCTest {
         VersionGarbageCollector.VersionGCStats stats = gc.gc(1, HOURS);
 
         assertStatsCountsEqual(stats,
-                noOldPropGc(0, 0, 0, 0, 0, 0, 0),
+                gapOrphOnly(),
+                gapOrphProp(),
+                allOrphProp(),
                 keepOneFull(0, 0, 1, 4,12, 0, 3),
                 keepOneUser(0, 0, 0, 4, 0, 0, 2),
-                betweenChkp(0, 0, 1, 0,18, 2, 3));
+                betweenChkp(),
+                unmergedBcs(0, 0, 1, 0,16, 2, 3),
+                btwnChkpUBC(0, 0, 1, 0,16, 2, 3));
         assertExists("1:/a");
         assertExists("1:/b");
 
@@ -337,8 +379,10 @@ public class BranchCommitGCTest {
         clock.waitUntil(clock.getTime() + HOURS.toMillis(2));
         stats = gc.gc(1, HOURS);
         assertStatsCountsZero(stats);
-        assertBranchRevisionRemovedFromAllDocuments(store, br1);
-        assertBranchRevisionRemovedFromAllDocuments(store, br2);
+        if (!isModeOneOf(DetailedGCMode.NONE)) {
+            assertBranchRevisionRemovedFromAllDocuments(store, br1);
+            assertBranchRevisionRemovedFromAllDocuments(store, br2);
+        }
     }
 
     @Test
@@ -382,10 +426,14 @@ public class BranchCommitGCTest {
         VersionGarbageCollector.VersionGCStats stats = gc.gc(1, HOURS);
 
         assertStatsCountsEqual(stats,
-                noOldPropGc(0, 0, 0, 0, 0, 0, 0),
+                gapOrphOnly(0, 0, 0, 0, 0, 0, 0),
+                gapOrphProp(0, 0, 0, 0, 0, 0, 0),
+                allOrphProp(0, 0, 0, 0, 0, 0, 0),
                 keepOneFull(0, 0, 1, 8,24, 0, 3),
                 keepOneUser(0, 0, 0, 8, 0, 0, 2),
-                betweenChkp(0, 0, 1, 0,35, 4, 3));
+                betweenChkp(),
+                unmergedBcs(0, 0, 1, 0,32, 4, 3),
+                btwnChkpUBC(0, 0, 1, 0,32, 4, 3));
         assertExists("1:/a");
         assertExists("1:/b");
 
@@ -393,10 +441,12 @@ public class BranchCommitGCTest {
         clock.waitUntil(clock.getTime() + HOURS.toMillis(2));
         stats = gc.gc(1, HOURS);
         assertStatsCountsZero(stats);
-        assertBranchRevisionRemovedFromAllDocuments(store, br1);
-        assertBranchRevisionRemovedFromAllDocuments(store, br2);
-        assertBranchRevisionRemovedFromAllDocuments(store, br3);
-        assertBranchRevisionRemovedFromAllDocuments(store, br4);
+        if (!isModeOneOf(DetailedGCMode.NONE)) {
+            assertBranchRevisionRemovedFromAllDocuments(store, br1);
+            assertBranchRevisionRemovedFromAllDocuments(store, br2);
+            assertBranchRevisionRemovedFromAllDocuments(store, br3);
+            assertBranchRevisionRemovedFromAllDocuments(store, br4);
+        }
     }
 
     @Test
@@ -418,17 +468,23 @@ public class BranchCommitGCTest {
 
         // first gc round now deletes it, via orphaned node deletion
         assertStatsCountsEqual(stats,
-                noOldPropGc(1, 0, 0, 0, 0, 0, 1),
+                gapOrphOnly(),
+                gapOrphProp(),
+                allOrphProp(1, 0, 0, 0, 0, 0, 1),
                 keepOneFull(1, 0, 0, 1, 6, 0, 4),
                 keepOneUser(1, 0, 0, 1, 0, 0, 2),
-                betweenChkp(1, 0, 0, 0, 7, 1, 4));
+                betweenChkp(1, 0, 0, 0, 0, 0, 1),
+                unmergedBcs(1, 0, 0, 0, 7, 1, 4),
+                btwnChkpUBC(1, 0, 0, 0, 7, 1, 4));
 
         // wait two hours
         clock.waitUntil(clock.getTime() + HOURS.toMillis(2));
         // now do second gc round - should not have anything left to clean up 
though
         stats = gc.gc(1, HOURS);
         assertStatsCountsZero(stats);
-        assertBranchRevisionRemovedFromAllDocuments(store, br);
+        if (!isModeOneOf(DetailedGCMode.NONE)) {
+            assertBranchRevisionRemovedFromAllDocuments(store, br);
+        }
     }
 
     @Test
@@ -450,11 +506,17 @@ public class BranchCommitGCTest {
         stats = gc.gc(1, HOURS);
 
         assertStatsCountsEqual(stats,
-                noOldPropGc(0, 0, 0, 0, 0, 0, 0),
+                gapOrphOnly(),
+                gapOrphProp(),
+                allOrphProp(),
                 keepOneFull(0, 0, 0, 1, 4, 0, 2),
                 keepOneUser(0, 0, 0, 1, 0, 0, 1),
-                betweenChkp(0, 0, 0, 1, 4, 1, 2));
-        assertBranchRevisionRemovedFromAllDocuments(store, br);
+                betweenChkp(),
+                unmergedBcs(0, 0, 0, 1, 4, 1, 2),
+                btwnChkpUBC(0, 0, 0, 1, 4, 1, 2));
+        if (!isModeOneOf(DetailedGCMode.NONE)) {
+            assertBranchRevisionRemovedFromAllDocuments(store, br);
+        }
     }
 
     @Test
@@ -470,11 +532,17 @@ public class BranchCommitGCTest {
 
         // 1 deleted prop: 1:/foo[a]
         assertStatsCountsEqual(stats,
-                noOldPropGc(0, 1, 0, 0, 0, 0, 1),
+                gapOrphOnly(),
+                gapOrphProp(0, 1, 0, 0, 0, 0, 1),
+                allOrphProp(0, 1, 0, 0, 0, 0, 1),
                 keepOneFull(0, 1, 0, 0, 4, 0, 2),
                 keepOneUser(0, 1, 0, 0, 0, 0, 1),
-                betweenChkp(0, 1, 0, 0, 4, 1, 2));
-        assertBranchRevisionRemovedFromAllDocuments(store, br);
+                betweenChkp(0, 1, 0, 0, 0, 0, 1),
+                unmergedBcs(0, 1, 0, 0, 4, 1, 2),
+                btwnChkpUBC(0, 1, 0, 0, 4, 1, 2));
+        if (!isModeOneOf(DetailedGCMode.NONE)) {
+            assertBranchRevisionRemovedFromAllDocuments(store, br);
+        }
     }
 
     @Test
@@ -504,18 +572,24 @@ public class BranchCommitGCTest {
         stats = gc.gc(1, HOURS);
 
         assertStatsCountsEqual(stats,
-                noOldPropGc(0, 0, 0, 0, 0, 0, 0),
+                gapOrphOnly(0, 0, 0, 0, 0, 0, 0),
+                gapOrphProp(0, 0, 0, 0, 0, 0, 0),
+                allOrphProp(0, 0, 0, 0, 0, 0, 0),
                 keepOneFull(0, 0, 1,10,40, 0, 2),
                 keepOneUser(0, 0, 0,10, 0, 0, 1),
-                betweenChkp(0, 0, 1, 0,59,10, 2));
+                betweenChkp(),
+                unmergedBcs(0, 0, 1, 0,50,10, 2),
+                btwnChkpUBC(0, 0, 1, 0,50,10, 2));
 
         doc = store.getDocumentStore().find(Collection.NODES, "1:/foo");
         Long finalModified = doc.getModified();
 
         assertEquals(originalModified, finalModified);
 
-        for (RevisionVector br : brs) {
-            assertBranchRevisionRemovedFromAllDocuments(store, br);
+        if (!isModeOneOf(DetailedGCMode.NONE)) {
+            for (RevisionVector br : brs) {
+                assertBranchRevisionRemovedFromAllDocuments(store, br);
+            }
         }
     }
 
@@ -547,12 +621,18 @@ public class BranchCommitGCTest {
         stats = gc.gc(1, HOURS);
 
         assertStatsCountsEqual(stats,
-                noOldPropGc(0, 0, 0, 0, 0, 0, 0),
+                gapOrphOnly(0, 0, 0, 0, 0, 0, 0),
+                gapOrphProp(0, 0, 0, 0, 0, 0, 0),
+                allOrphProp(0, 0, 0, 0, 0, 0, 0),
                 keepOneFull(0, 0, 2,10,30, 0, 2),
                 keepOneUser(0, 0, 0,10, 0, 0, 1),
-                betweenChkp(0, 0, 2, 0,59,10, 2));
-        for (RevisionVector br : brs) {
-            assertBranchRevisionRemovedFromAllDocuments(store, br);
+                betweenChkp(),
+                unmergedBcs(0, 0, 2, 0,40,10, 2),
+                btwnChkpUBC(0, 0, 2, 0,40,10, 2));
+        if (!isModeOneOf(DetailedGCMode.NONE)) {
+            for (RevisionVector br : brs) {
+                assertBranchRevisionRemovedFromAllDocuments(store, br);
+            }
         }
     }
 
@@ -600,17 +680,26 @@ public class BranchCommitGCTest {
         // deleted internal prop : 0:/ _collision
         // deleted properties are 0:/ -> rootProp, _collisions & 1:/foo -> a
         assertStatsCountsEqual(stats,
-                noOldPropGc(0, 2, 0, 0, 0, 0, 2),
+                gapOrphOnly(),
+                gapOrphProp(0, 2, 0, 0, 0, 0, 2),
+                allOrphProp(0, 2, 0, 0, 0, 0, 2),
                 keepOneFull(0, 2, 1, 0, 8, 0, 2),
                 keepOneUser(0, 2, 0, 0, 0, 0, 2),
-                betweenChkp(0, 2, 1, 0, 5, 1, 2));
+                betweenChkp(0, 2, 0, 0, 0, 0, 2),
+                unmergedBcs(0, 2, 1, 0, 4, 1, 2),
+                btwnChkpUBC(0, 2, 1, 0, 4, 1, 2));
+        if (isModeOneOf(DetailedGCMode.NONE, DetailedGCMode.GAP_ORPHANS)) {
+            return;
+        }
+
         {
             // some flakyness diagnostics
             NodeDocument d = store.getDocumentStore().find(Collection.NODES, 
"0:/", -1);
             assertNotNull(d);
             assertEquals(0, d.getLocalMap("rootProp").size());
-            if (VersionGarbageCollector.getRevisionDetailedGcType() == 
RDGCType.KEEP_ONE_FULL_MODE
-                    || VersionGarbageCollector.getRevisionDetailedGcType() == 
RDGCType.OLDER_THAN_24H_AND_BETWEEN_CHECKPOINTS_MODE) {
+            if (VersionGarbageCollector.getDetailedGcMode() == 
DetailedGCMode.ORPHANS_EMPTYPROPS_KEEP_ONE_ALL_PROPS
+                    || VersionGarbageCollector.getDetailedGcMode() == 
DetailedGCMode.ORPHANS_EMPTYPROPS_UNMERGED_BC
+                    || VersionGarbageCollector.getDetailedGcMode() == 
DetailedGCMode.ORPHANS_EMPTYPROPS_BETWEEN_CHECKPOINTS_WITH_UNMERGED_BC) {
                 assertEquals(0, d.getLocalMap("_collisions").size());
             } else {
                 assertEquals(1, d.getLocalMap("_collisions").size());
diff --git 
a/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/DetailGCHelper.java
 
b/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/DetailGCHelper.java
index 6bbda403d6..ec7b792066 100644
--- 
a/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/DetailGCHelper.java
+++ 
b/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/DetailGCHelper.java
@@ -18,7 +18,7 @@
  */
 package org.apache.jackrabbit.oak.plugins.document;
 
-import 
org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector.RDGCType;
+import 
org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector.DetailedGCMode;
 import org.apache.jackrabbit.oak.plugins.document.util.Utils;
 import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
 import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
@@ -74,7 +74,10 @@ public class DetailGCHelper {
             final DocumentNodeStore store, final RevisionVector br,
             final String... exceptIds) {
         assertTrue(br.isBranch());
-        if (VersionGarbageCollector.getRevisionDetailedGcType() == 
RDGCType.NO_OLD_PROP_REV_GC) {
+        if (VersionGarbageCollector.getDetailedGcMode() == 
DetailedGCMode.GAP_ORPHANS
+                || VersionGarbageCollector.getDetailedGcMode() == 
DetailedGCMode.GAP_ORPHANS_EMPTYPROPS
+                || VersionGarbageCollector.getDetailedGcMode() == 
DetailedGCMode.ALL_ORPHANS_EMPTYPROPS
+                || VersionGarbageCollector.getDetailedGcMode() == 
DetailedGCMode.ORPHANS_EMPTYPROPS_BETWEEN_CHECKPOINTS_NO_UNMERGED_BC) {
             // then we must skip these asserts, as we cannot guarantee
             // that all revisions are cleaned up in this mode
             return;
@@ -96,7 +99,7 @@ public class DetailGCHelper {
             for (Entry<String, Object> e : target.data.entrySet()) {
                 String k = e.getKey();
                 final boolean internal = k.startsWith("_");
-                final boolean dgcSupportsInternalPropCleanup = 
(VersionGarbageCollector.getRevisionDetailedGcType() != 
RDGCType.KEEP_ONE_CLEANUP_USER_PROPERTIES_ONLY_MODE);
+                final boolean dgcSupportsInternalPropCleanup = 
(VersionGarbageCollector.getDetailedGcMode() != 
DetailedGCMode.ORPHANS_EMPTYPROPS_KEEP_ONE_USER_PROPS);
                 if (internal && !dgcSupportsInternalPropCleanup) {
                     // skip
                     continue;
diff --git 
a/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/VersionGarbageCollectorIT.java
 
b/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/VersionGarbageCollectorIT.java
index 694124c468..5ba734fdf6 100644
--- 
a/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/VersionGarbageCollectorIT.java
+++ 
b/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/VersionGarbageCollectorIT.java
@@ -113,7 +113,7 @@ import org.apache.jackrabbit.oak.api.PropertyState;
 import org.apache.jackrabbit.oak.api.Type;
 import 
org.apache.jackrabbit.oak.plugins.document.DocumentStoreFixture.RDBFixture;
 import 
org.apache.jackrabbit.oak.plugins.document.FailingDocumentStore.FailedUpdateOpListener;
-import 
org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector.RDGCType;
+import 
org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector.DetailedGCMode;
 import 
org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector.VersionGCStats;
 import 
org.apache.jackrabbit.oak.plugins.document.bundlor.BundlingConfigInitializer;
 import org.apache.jackrabbit.oak.plugins.document.mongo.MongoTestUtils;
@@ -140,12 +140,16 @@ import org.junit.runners.Parameterized.Parameter;
 public class VersionGarbageCollectorIT {
 
     static class GCCounts {
-        RDGCType mode;
+        private final DetailedGCMode mode;
         int deletedDocGCCount, deletedPropsCount, deletedInternalPropsCount,
                 deletedPropRevsCount, deletedInternalPropRevsCount,
                 deletedUnmergedBCCount, updatedDetailedGCDocsCount;
 
-        public GCCounts(RDGCType mode, int deletedDocGCCount, int 
deletedPropsCount,
+        public GCCounts(DetailedGCMode mode) {
+            this(mode, 0, 0, 0, 0, 0, 0, 0);
+        }
+
+        public GCCounts(DetailedGCMode mode, int deletedDocGCCount, int 
deletedPropsCount,
                 int deletedInternalPropsCount, int deletedPropRevsCount,
                 int deletedInternalPropRevsCount, int deletedUnmergedBCCount,
                 int updatedDetailedGCDocsCount) {
@@ -157,11 +161,13 @@ public class VersionGarbageCollectorIT {
             assertTrue(deletedInternalPropRevsCount != -1);
             assertTrue(deletedUnmergedBCCount != -1);
             assertTrue(updatedDetailedGCDocsCount != -1);
-            if (mode == RDGCType.NO_OLD_PROP_REV_GC) {
+            if (mode == DetailedGCMode.GAP_ORPHANS
+                    || mode == DetailedGCMode.GAP_ORPHANS_EMPTYPROPS
+                    || mode == DetailedGCMode.ALL_ORPHANS_EMPTYPROPS) {
                 assertEquals(0, deletedPropRevsCount);
                 assertEquals(0, deletedInternalPropRevsCount);
                 assertEquals(0, deletedUnmergedBCCount);
-            } else if (mode == 
RDGCType.KEEP_ONE_CLEANUP_USER_PROPERTIES_ONLY_MODE) {
+            } else if (mode == 
DetailedGCMode.ORPHANS_EMPTYPROPS_KEEP_ONE_USER_PROPS) {
                 assertEquals(0, deletedInternalPropsCount);
                 assertEquals(0, deletedInternalPropRevsCount);
             }
@@ -179,7 +185,7 @@ public class VersionGarbageCollectorIT {
     public DocumentStoreFixture fixture;
 
     @Parameter(1)
-    public RDGCType gcType;
+    public DetailedGCMode detailedGcMode;
 
     private Clock clock;
 
@@ -193,14 +199,14 @@ public class VersionGarbageCollectorIT {
 
     private ExecutorService execService;
 
-    private RDGCType originalGcType;
+    private DetailedGCMode originalDetailedGcMode;
 
     @Parameterized.Parameters(name="{index}: {0} with {1}")
     public static java.util.Collection<Object[]> params() throws IOException {
         java.util.Collection<Object[]> params = new LinkedList<>();
         for (Object[] fixture : AbstractDocumentStoreTest.fixtures()) {
             DocumentStoreFixture f = (DocumentStoreFixture)fixture[0];
-            for (RDGCType gcType : RDGCType.values()) {
+            for (DetailedGCMode gcType : DetailedGCMode.values()) {
                 params.add(new Object[] {f, gcType});
             }
         }
@@ -225,13 +231,13 @@ public class VersionGarbageCollectorIT {
         MongoTestUtils.setReadPreference(store1, ReadPreference.primary());
         gc = store1.getVersionGarbageCollector();
 
-        originalGcType = VersionGarbageCollector.getRevisionDetailedGcType();
-        writeStaticField(VersionGarbageCollector.class, 
"revisionDetailedGcType", gcType, true);
+        originalDetailedGcMode = VersionGarbageCollector.getDetailedGcMode();
+        writeStaticField(VersionGarbageCollector.class, "detailedGcMode", 
detailedGcMode, true);
     }
 
     @After
     public void tearDown() throws Exception {
-        writeStaticField(VersionGarbageCollector.class, 
"revisionDetailedGcType", originalGcType, true);
+        writeStaticField(VersionGarbageCollector.class, "detailedGcMode", 
originalDetailedGcMode, true);
         if (store2 != null) {
             store2.dispose();
         }
@@ -414,7 +420,15 @@ public class VersionGarbageCollectorIT {
         VersionGCStats stats = gc(gc, maxAge, TimeUnit.MILLISECONDS);
         assertFalse("Detailed GC should be performed", 
stats.ignoredDetailedGCDueToCheckPoint);
         assertFalse(stats.canceled);
-        assertEquals(batchSize, stats.updatedDetailedGCDocsCount);
+        assertStatsCountsEqual(stats,
+                gapOrphOnly(),
+                gapOrphProp(0, (int)batchSize, 0, 0, 0, 0, (int)batchSize),
+                allOrphProp(0, (int)batchSize, 0, 0, 0, 0, (int)batchSize),
+                keepOneFull(0, (int)batchSize, 0, 0, 0, 0, (int)batchSize),
+                keepOneUser(0, (int)batchSize, 0, 0, 0, 0, (int)batchSize),
+                unmergedBcs(0, (int)batchSize, 0, 0, 0, 0, (int)batchSize),
+                betweenChkp(0, (int)batchSize, 0, 0, 2, 0, (int)batchSize + 1),
+                btwnChkpUBC(0, (int)batchSize, 0, 0, 2, 0, (int)batchSize + 
1));
         assertFalse(stats.needRepeat);
     }
 
@@ -485,10 +499,14 @@ public class VersionGarbageCollectorIT {
         clock.waitUntil(clock.getTime() + delta*2);
         VersionGCStats stats = gc(gc, delta, MILLISECONDS);
         assertStatsCountsEqual(stats,
-                noOldPropGc(0, 1, 0, 0, 0, 0, 1),
+                gapOrphOnly(),
+                gapOrphProp(0, 1, 0, 0, 0, 0, 1),
+                allOrphProp(0, 1, 0, 0, 0, 0, 1),
                 keepOneFull(0, 1, 0, 0, 0, 0, 1),
                 keepOneUser(0, 1, 0, 0, 0, 0, 1),
-                betweenChkp(0, 1, 0, 0, 0, 0, 1));
+                unmergedBcs(0, 1, 0, 0, 0, 0, 1),
+                betweenChkp(0, 1, 0, 0, 0, 0, 1),
+                btwnChkpUBC(0, 1, 0, 0, 0, 0, 1));
         assertTrue(stats.ignoredGCDueToCheckPoint);
         assertTrue(stats.ignoredDetailedGCDueToCheckPoint);
         assertTrue(stats.canceled);
@@ -541,10 +559,14 @@ public class VersionGarbageCollectorIT {
 
         stats = gc(gc, maxAge*2, HOURS);
         assertStatsCountsEqual(stats,
-                noOldPropGc(0, 1, 0, 0, 0, 0, 1),
+                gapOrphOnly(),
+                gapOrphProp(0, 1, 0, 0, 0, 0, 1),
+                allOrphProp(0, 1, 0, 0, 0, 0, 1),
                 keepOneFull(0, 1, 0, 0, 0, 0, 1),
                 keepOneUser(0, 1, 0, 0, 0, 0, 1),
-                betweenChkp(0, 1, 0, 0, 0, 0, 1));
+                unmergedBcs(0, 1, 0, 0, 0, 0, 1),
+                betweenChkp(0, 1, 0, 0, 3, 0, 2),
+                btwnChkpUBC(0, 1, 0, 0, 3, 0, 2));
         assertEquals(MIN_ID_VALUE, stats.oldestModifiedDocId);
 
         //4. Check that a revived property (deleted and created again) does 
not get gc
@@ -560,10 +582,14 @@ public class VersionGarbageCollectorIT {
 
         stats = gc(gc, maxAge*2, HOURS);
         assertStatsCountsEqual(stats,
-                noOldPropGc(0, 0, 0, 0, 0, 0, 0),
+                gapOrphOnly(),
+                gapOrphProp(),
+                allOrphProp(),
                 keepOneFull(0, 0, 0, 2, 0, 0, 1),
                 keepOneUser(0, 0, 0, 2, 0, 0, 1),
-                betweenChkp(0, 0, 0, 0, 0, 0, 0));
+                unmergedBcs(),
+                betweenChkp(0, 0, 0, 1, 0, 0, 1),
+                btwnChkpUBC(0, 0, 0, 1, 0, 0, 1));
         assertEquals(MIN_ID_VALUE, stats.oldestModifiedDocId);
     }
 
@@ -601,10 +627,14 @@ public class VersionGarbageCollectorIT {
 
         VersionGCStats stats = gc(gc, maxAge*2, HOURS);
         assertStatsCountsEqual(stats,
-                noOldPropGc(0, 50_000, 0, 0, 0, 0, 5_000),
+                gapOrphOnly(),
+                gapOrphProp(0, 50_000, 0, 0, 0, 0, 5_000),
+                allOrphProp(0, 50_000, 0, 0, 0, 0, 5_000),
                 keepOneFull(0, 50_000, 0, 0, 0, 0, 5_000),
                 keepOneUser(0, 50_000, 0, 0, 0, 0, 5_000),
-                betweenChkp(0, 50_000, 0, 0, 0, 0, 5_000));
+                unmergedBcs(0, 50_000, 0, 0, 0, 0, 5_000),
+                betweenChkp(0, 50_000, 0, 0, 2, 0, 5_000 + 1),
+                btwnChkpUBC(0, 50_000, 0, 0, 2, 0, 5_000 + 1));
         assertEquals(MIN_ID_VALUE, stats.oldestModifiedDocId);
     }
 
@@ -647,10 +677,14 @@ public class VersionGarbageCollectorIT {
 
         VersionGCStats stats = gc(gc, maxAge, HOURS);
         assertStatsCountsEqual(stats,
-                noOldPropGc(0, 50_000, 0, 0, 0, 0, 5_000),
+                gapOrphOnly(),
+                gapOrphProp(0, 50_000, 0, 0, 0, 0, 5_000),
+                allOrphProp(0, 50_000, 0, 0, 0, 0, 5_000),
                 keepOneFull(0, 50_000, 0, 0, 0, 0, 5_000),
                 keepOneUser(0, 50_000, 0, 0, 0, 0, 5_000),
-                betweenChkp(0, 50_000, 0, 0, 0, 0, 5_000));
+                unmergedBcs(0, 50_000, 0, 0, 0, 0, 5_000),
+                betweenChkp(0, 50_000, 0, 0, 51, 0, 5_000 + 1),
+                btwnChkpUBC(0, 50_000, 0, 0, 51, 0, 5_000 + 1));
         assertEquals(MIN_ID_VALUE, stats.oldestModifiedDocId);
     }
 
@@ -727,10 +761,14 @@ public class VersionGarbageCollectorIT {
 
         stats = gc(gc, maxAge*2, HOURS);
         assertStatsCountsEqual(stats,
-                noOldPropGc(0, 10, 0, 0, 0, 0, 10),
+                gapOrphOnly(),
+                gapOrphProp(0, 10, 0, 0, 0, 0, 10),
+                allOrphProp(0, 10, 0, 0, 0, 0, 10),
                 keepOneFull(0, 10, 0, 0, 0, 0, 10),
                 keepOneUser(0, 10, 0, 0, 0, 0, 10),
-                betweenChkp(0, 10, 0, 0, 0, 0, 10));
+                unmergedBcs(0, 10, 0, 0, 0, 0, 10),
+                betweenChkp(0, 10, 0, 0, 2, 0, 11),
+                btwnChkpUBC(0, 10, 0, 0, 2, 0, 11));
         assertEquals(MIN_ID_VALUE, stats.oldestModifiedDocId);
 
         //3. now reCreate those properties again
@@ -755,10 +793,14 @@ public class VersionGarbageCollectorIT {
         clock.waitUntil(clock.getTime() + HOURS.toMillis(maxAge*2) + delta);
         stats = gc(gc, maxAge*2, HOURS);
         assertStatsCountsEqual(stats,
-                noOldPropGc(0, 10, 0, 0, 0, 0, 10),
+                gapOrphOnly(),
+                gapOrphProp(0, 10, 0, 0, 0, 0, 10),
+                allOrphProp(0, 10, 0, 0, 0, 0, 10),
                 keepOneFull(0, 10, 0, 0, 0, 0, 10),
                 keepOneUser(0, 10, 0, 0, 0, 0, 10),
-                betweenChkp(0, 10, 0, 0, 0, 0, 10));
+                unmergedBcs(0, 10, 0, 0, 0, 0, 10),
+                betweenChkp(0, 10, 0, 0, 2, 0, 11),
+                btwnChkpUBC(0, 10, 0, 0, 2, 0, 11));
         assertEquals(MIN_ID_VALUE, stats.oldestModifiedDocId);
     }
 
@@ -829,10 +871,14 @@ public class VersionGarbageCollectorIT {
         clock.waitUntil(clock.getTime() + HOURS.toMillis(maxAge*2) + delta);
         VersionGCStats stats = gc(gc, maxAge*2, HOURS);
         assertStatsCountsEqual(stats,
-                noOldPropGc(0, 10, 0, 0, 0, 0, 10),
+                gapOrphOnly(),
+                gapOrphProp(0, 10, 0, 0, 0, 0, 10),
+                allOrphProp(0, 10, 0, 0, 0, 0, 10),
                 keepOneFull(0, 10, 0, 0, 0, 0, 10),
                 keepOneUser(0, 10, 0, 0, 0, 0, 10),
-                betweenChkp(0, 10, 0, 0, 0, 0, 10));
+                unmergedBcs(0, 10, 0, 0, 0, 0, 10),
+                betweenChkp(0, 10, 0, 0, 2, 0, 11),
+                btwnChkpUBC(0, 10, 0, 0, 2, 0, 11));
         assertEquals(MIN_ID_VALUE, stats.oldestModifiedDocId);
     }
 
@@ -876,10 +922,14 @@ public class VersionGarbageCollectorIT {
 
         stats = gc(gc, maxAge*2, HOURS);
         assertStatsCountsEqual(stats,
-                noOldPropGc(0, 10, 0, 0, 0, 0, 1),
+                gapOrphOnly(),
+                gapOrphProp(0, 10, 0, 0, 0, 0, 1),
+                allOrphProp(0, 10, 0, 0, 0, 0, 1),
                 keepOneFull(0, 10, 0, 0, 0, 0, 1),
                 keepOneUser(0, 10, 0, 0, 0, 0, 1),
-                betweenChkp(0, 10, 0, 0, 0, 0, 1));
+                unmergedBcs(0, 10, 0, 0, 0, 0, 1),
+                betweenChkp(0, 10, 0, 0, 1, 0, 2),
+                btwnChkpUBC(0, 10, 0, 0, 1, 0, 2));
 
         //4. Check that a revived property (deleted and created again) does 
not get gc
         NodeBuilder b4 = store1.getRoot().builder();
@@ -890,7 +940,15 @@ public class VersionGarbageCollectorIT {
 
         clock.waitUntil(clock.getTime() + HOURS.toMillis(maxAge*2) + delta);
         stats = gc(gc, maxAge*2, HOURS);
-        assertStatsCountsZero(stats);
+        assertStatsCountsEqual(stats,
+                gapOrphOnly(),
+                gapOrphProp(),
+                allOrphProp(),
+                keepOneFull(),
+                keepOneUser(),
+                unmergedBcs(),
+                betweenChkp(0, 0, 0, 0, 1, 0, 1),
+                btwnChkpUBC(0, 0, 0, 0, 1, 0, 1));
     }
 
     @Test
@@ -935,10 +993,14 @@ public class VersionGarbageCollectorIT {
 
         stats = gc(gc, maxAge*2, HOURS);
         assertStatsCountsEqual(stats,
-                noOldPropGc(0, 10, 0, 0, 0, 0, 1),
+                gapOrphOnly(),
+                gapOrphProp(0, 10, 0, 0, 0, 0, 1),
+                allOrphProp(0, 10, 0, 0, 0, 0, 1),
                 keepOneFull(0, 10, 0, 0, 0, 0, 1),
                 keepOneUser(0, 10, 0, 0, 0, 0, 1),
-                betweenChkp(0, 10, 0, 0, 0, 0, 1));
+                unmergedBcs(0, 10, 0, 0, 0, 0, 1),
+                betweenChkp(0, 10, 0, 0, 1, 0, 2),
+                btwnChkpUBC(0, 10, 0, 0, 1, 0, 2));
 
         //4. Check that a revived property (deleted and created again) does 
not get gc
         NodeBuilder b4 = store1.getRoot().builder();
@@ -949,7 +1011,15 @@ public class VersionGarbageCollectorIT {
 
         clock.waitUntil(clock.getTime() + HOURS.toMillis(maxAge*2) + delta);
         stats = gc(gc, maxAge*2, HOURS);
-        assertStatsCountsZero(stats);
+        assertStatsCountsEqual(stats,
+                gapOrphOnly(),
+                gapOrphProp(),
+                allOrphProp(),
+                keepOneFull(),
+                keepOneUser(),
+                unmergedBcs(),
+                betweenChkp(0, 0, 0, 0, 1, 0, 1),
+                btwnChkpUBC(0, 0, 0, 0, 1, 0, 1));
     }
 
     @Test
@@ -1003,10 +1073,14 @@ public class VersionGarbageCollectorIT {
 
         stats = gc(gc, maxAge*2, HOURS);
         assertStatsCountsEqual(stats,
-                noOldPropGc(0, 10, 0, 0, 0, 0, 1),
+                gapOrphOnly(),
+                gapOrphProp(0, 10, 0, 0, 0, 0, 1),
+                allOrphProp(0, 10, 0, 0, 0, 0, 1),
                 keepOneFull(0, 10, 0, 0, 0, 0, 1),
                 keepOneUser(0, 10, 0, 0, 0, 0, 1),
-                betweenChkp(0, 10, 0, 0, 0, 0, 1));
+                unmergedBcs(0, 10, 0, 0, 0, 0, 1),
+                betweenChkp(0, 10, 0, 0, 1, 0, 2),
+                btwnChkpUBC(0, 10, 0, 0, 1, 0, 2));
     }
 
     @Test
@@ -1088,10 +1162,14 @@ public class VersionGarbageCollectorIT {
 
         stats = gc(gc, maxAge*2, HOURS);
         assertStatsCountsEqual(stats,
-                noOldPropGc(0, 10, 0, 0, 0, 0, 1),
+                gapOrphOnly(),
+                gapOrphProp(0, 10, 0, 0, 0, 0, 1),
+                allOrphProp(0, 10, 0, 0, 0, 0, 1),
                 keepOneFull(0, 10, 0, 0, 0, 0, 1),
                 keepOneUser(0, 10, 0, 0, 0, 0, 1),
-                betweenChkp(0, 10, 0, 0, 0, 0, 1));
+                unmergedBcs(0, 10, 0, 0, 0, 0, 1),
+                betweenChkp(0, 10, 0, 0, 1, 0, 2),
+                btwnChkpUBC(0, 10, 0, 0, 1, 0, 2));
 
         x = store1.getRoot().getChildNode("x");
         assertTrue(x.exists());
@@ -1104,28 +1182,30 @@ public class VersionGarbageCollectorIT {
     }
 
     static void assertStatsCountsZero(VersionGCStats stats) {
-        GCCounts c = new 
GCCounts(VersionGarbageCollector.getRevisionDetailedGcType(),
-                0, 0, 0, 0, 0, 0, 0);
+        GCCounts c = new GCCounts(VersionGarbageCollector.getDetailedGcMode());
         assertStatsCountsEqual(stats, c);
     }
 
     static void assertStatsCountsEqual(VersionGCStats stats, GCCounts... 
counts) {
         GCCounts c = null;
         for (GCCounts a : counts) {
-            if (a.mode == VersionGarbageCollector.getRevisionDetailedGcType()) 
{
+            if (a.mode == VersionGarbageCollector.getDetailedGcMode()) {
                 c = a;
                 break;
             }
         }
+        if (c == null && VersionGarbageCollector.getDetailedGcMode() == 
DetailedGCMode.NONE) {
+            c = new GCCounts(DetailedGCMode.NONE);
+        }
         assertNotNull(stats);
         assertNotNull(c);
-        assertEquals(c.deletedDocGCCount, stats.deletedDocGCCount);
-        assertEquals(c.deletedPropsCount, stats.deletedPropsCount);
-        assertEquals(c.deletedInternalPropsCount, 
stats.deletedInternalPropsCount);
-        assertEquals(c.deletedPropRevsCount, stats.deletedPropRevsCount);
-        assertEquals(c.deletedInternalPropRevsCount, 
stats.deletedInternalPropRevsCount);
-        assertEquals(c.deletedUnmergedBCCount, stats.deletedUnmergedBCCount);
-        assertEquals(c.updatedDetailedGCDocsCount, 
stats.updatedDetailedGCDocsCount);
+        assertEquals(c.mode + "/docGC", c.deletedDocGCCount, 
stats.deletedDocGCCount);
+        assertEquals(c.mode + "/props", c.deletedPropsCount, 
stats.deletedPropsCount);
+        assertEquals(c.mode + "/internalProps", c.deletedInternalPropsCount, 
stats.deletedInternalPropsCount);
+        assertEquals(c.mode + "/propRevs", c.deletedPropRevsCount, 
stats.deletedPropRevsCount);
+        assertEquals(c.mode + "/internalPropRevs", 
c.deletedInternalPropRevsCount, stats.deletedInternalPropRevsCount);
+        assertEquals(c.mode + "/unmergedBC", c.deletedUnmergedBCCount, 
stats.deletedUnmergedBCCount);
+        assertEquals(c.mode + "/updatedDetailedGCDocsCount", 
c.updatedDetailedGCDocsCount, stats.updatedDetailedGCDocsCount);
     }
 
     @Test
@@ -1324,10 +1404,14 @@ public class VersionGarbageCollectorIT {
         assertNotNull(ckAfter);
         assertEquals(ckBefore.getValue(Type.STRING), 
ckAfter.getValue(Type.STRING));
         assertStatsCountsEqual(stats,
-                noOldPropGc(0, 0, 0, 0, 0, 0, 0),
+                gapOrphOnly(),
+                gapOrphProp(),
+                allOrphProp(),
                 keepOneFull(0, 0, 0, 3, 0, 0, 2),
                 keepOneUser(0, 0, 0, 3, 0, 0, 2),
-                betweenChkp(0, 0, 0, 0, 0, 0, 0));
+                unmergedBcs(),
+                betweenChkp(0, 0, 0, 1, 1, 0, 2),
+                btwnChkpUBC(0, 0, 0, 1, 1, 0, 2));
     }
 
     @Test
@@ -1359,10 +1443,14 @@ public class VersionGarbageCollectorIT {
         // now the GC
         VersionGCStats stats = gc(gc, 1, HOURS);
         assertStatsCountsEqual(stats,
-                noOldPropGc(0, 0, 0, 0, 0, 0, 0),
+                gapOrphOnly(),
+                gapOrphProp(),
+                allOrphProp(),
                 keepOneFull(0, 0, 0, 1, 0, 0, 1),
                 keepOneUser(0, 0, 0, 1, 0, 0, 1),
-                betweenChkp(0, 0, 0, 0, 0, 0, 0));
+                unmergedBcs(),
+                betweenChkp(0, 0, 0, 0, 1, 0, 1),
+                btwnChkpUBC(0, 0, 0, 0, 1, 0, 1));
     }
 
     @Test
@@ -1428,8 +1516,7 @@ public class VersionGarbageCollectorIT {
         // create branch commits
         RevisionVector br1 = unmergedBranchCommit(store1, b -> 
b.child("node1").setProperty("a", "2"));
         store1.runBackgroundOperations();
-        store1.invalidateNodeChildrenCache();
-        store1.getNodeCache().invalidateAll();
+        invalidateCaches(store1);
         assertEquals("1", 
store1.getRoot().getChildNode("node1").getProperty("a").getValue(Type.STRING));
 
         // enable the detailed gc flag
@@ -1438,8 +1525,7 @@ public class VersionGarbageCollectorIT {
         // wait two hours
         clock.waitUntil(clock.getTime() + HOURS.toMillis(2));
 
-        store1.invalidateNodeChildrenCache();
-        store1.getNodeCache().invalidateAll();
+        invalidateCaches(store1);
         assertEquals("1", 
store1.getRoot().getChildNode("node1").getProperty("a").getValue(Type.STRING));
 
         // clean everything older than one hour
@@ -1449,37 +1535,36 @@ public class VersionGarbageCollectorIT {
         nb.child("node1").setProperty("b", "4");
         store1.merge(nb, EmptyHook.INSTANCE, CommitInfo.EMPTY);
         VersionGCStats stats = gc(gc, 1, HOURS);
-        store1.invalidateNodeChildrenCache();
-        store1.getNodeCache().invalidateAll();
+        invalidateCaches(store1);
         assertEquals("1", 
store1.getRoot().getChildNode("node1").getProperty("a").getValue(Type.STRING));
         store1.runBackgroundOperations();
-        store1.invalidateNodeChildrenCache();
-        store1.getNodeCache().invalidateAll();
+        invalidateCaches(store1);
         assertEquals("1", 
store1.getRoot().getChildNode("node1").getProperty("a").getValue(Type.STRING));
         store1.runBackgroundOperations();
-        store1.invalidateNodeChildrenCache();
-        store1.getNodeCache().invalidateAll();
+        invalidateCaches(store1);
         assertEquals("1", 
store1.getRoot().getChildNode("node1").getProperty("a").getValue(Type.STRING));
         createSecondaryStore(LeaseCheckMode.LENIENT);
 
         // while "2" was written to node1/a via an unmerged branch commit,
         // it should not have been made visible through DGC/sweep combo
-        store2.invalidateNodeChildrenCache();
-        store2.getNodeCache().invalidateAll();
+        invalidateCaches(store2);
         assertEquals("1", 
store2.getRoot().getChildNode("node1").getProperty("a").getValue(Type.STRING));
         assertEquals("4", 
store2.getRoot().getChildNode("node1").getProperty("b").getValue(Type.STRING));
-        store2.invalidateNodeChildrenCache();
-        store2.getNodeCache().invalidateAll();
+        invalidateCaches(store2);
         assertEquals("1", 
store2.getRoot().getChildNode("node1").getProperty("a").getValue(Type.STRING));
         assertEquals("4", 
store2.getRoot().getChildNode("node1").getProperty("b").getValue(Type.STRING));
 
         // deletedPropsCount=0 : _bc on /node1 and / CANNOT be removed
         // deletedPropRevsCount=1 : (nothing on /node1[a, _commitRoot), 
/[_revisions]
         assertStatsCountsEqual(stats,
-                noOldPropGc(0, 0, 0, 0, 0, 0, 0),
+                gapOrphOnly(),
+                gapOrphProp(),
+                allOrphProp(),
                 keepOneFull(0, 0, 1, 0, 1, 0, 1),
-                keepOneUser(0, 0, 0, 0, 0, 0, 0),
-                betweenChkp(0, 0, 1, 0, 2, 1, 1));
+                keepOneUser(),
+                unmergedBcs(0, 0, 1, 0, 1, 1, 1),
+                betweenChkp(0, 0, 0, 0, 1, 0, 1),
+                btwnChkpUBC(0, 0, 1, 0, 2, 1, 1));
         // checking for br1 revisino to have disappeared doesn't really make 
much sense,
         // since 1:/node1 isn't GCed as it is young, and 0:/ being root cannot 
guarantee full removal
         // (if br1 is deleted form 0:/ _bc, then the commit value resolution 
flips it to committed)
@@ -1518,12 +1603,18 @@ public class VersionGarbageCollectorIT {
         VersionGCStats stats = gc(gc, 1, HOURS);
 
         assertStatsCountsEqual(stats,
-                noOldPropGc(0, 3, 0, 0, 0, 0, 2),
+                gapOrphOnly(),
+                gapOrphProp(0, 3, 0, 0, 0, 0, 2),
+                allOrphProp(0, 3, 0, 0, 0, 0, 2),
                 keepOneFull(0, 3, 1, 1, 9, 0, 3),
                 keepOneUser(0, 3, 0, 1, 0, 0, 2),
-                betweenChkp(0, 3, 1, 1, 8, 2, 3));
-        assertBranchRevisionRemovedFromAllDocuments(store1, br1);
-        assertBranchRevisionRemovedFromAllDocuments(store1, br4);
+                unmergedBcs(0, 3, 1, 1, 7, 2, 3),
+                betweenChkp(0, 3, 0, 0, 1, 0, 3),
+                btwnChkpUBC(0, 3, 1, 1, 8, 2, 3));
+        if (!isModeOneOf(DetailedGCMode.NONE, DetailedGCMode.GAP_ORPHANS, 
DetailedGCMode.GAP_ORPHANS_EMPTYPROPS)) {
+            assertBranchRevisionRemovedFromAllDocuments(store1, br1);
+            assertBranchRevisionRemovedFromAllDocuments(store1, br4);
+        }
     }
 
     @Test
@@ -1559,15 +1650,31 @@ public class VersionGarbageCollectorIT {
 
         VersionGCStats stats = gc(gc, 1, HOURS);
         assertStatsCountsEqual(stats,
-                noOldPropGc(0, 3, 0, 0,  0, 0, 2),
+                gapOrphOnly(),
+                gapOrphProp(0, 3, 0, 0,  0, 0, 2),
+                allOrphProp(0, 3, 0, 0,  0, 0, 2),
                 keepOneFull(0, 3, 2, 1, 17, 0, 3),
                 keepOneUser(0, 3, 0, 1,  0, 0, 2),
-                betweenChkp(0, 3, 2, 1, 18, 4, 3));
-        assertBranchRevisionRemovedFromAllDocuments(store1, br1);
-        assertBranchRevisionRemovedFromAllDocuments(store1, br2);
-        assertBranchRevisionRemovedFromAllDocuments(store1, br3);
-        assertBranchRevisionRemovedFromAllDocuments(store1, br4);
+                unmergedBcs(0, 3, 2, 1, 15, 4, 3),
+                betweenChkp(0, 3, 0, 0,  1, 0, 3),
+                btwnChkpUBC(0, 3, 2, 1, 16, 4, 3));
+        if (!isModeOneOf(DetailedGCMode.NONE, DetailedGCMode.GAP_ORPHANS, 
DetailedGCMode.GAP_ORPHANS_EMPTYPROPS)) {
+            assertBranchRevisionRemovedFromAllDocuments(store1, br1);
+            assertBranchRevisionRemovedFromAllDocuments(store1, br2);
+            assertBranchRevisionRemovedFromAllDocuments(store1, br3);
+            assertBranchRevisionRemovedFromAllDocuments(store1, br4);
+        }
+    }
+
+    static boolean isModeOneOf(DetailedGCMode... modes) {
+        for (DetailedGCMode rdgcType : modes) {
+            if (VersionGarbageCollector.getDetailedGcMode() == rdgcType) {
+                return true;
+            }
+        }
+        return false;
     }
+
     // OAK-8646 END
 
     /**
@@ -1575,7 +1682,15 @@ public class VersionGarbageCollectorIT {
      */
     @Test
     public void lateWriteCreateChildGC() throws Exception {
-        doLateWriteCreateChildrenGC(of("/grand/parent"), 
of("/grand/parent/a"), 1, "/d");
+        doLateWriteCreateChildrenGC(of("/grand/parent"), 
of("/grand/parent/a"), "/d",
+            gapOrphOnly(),
+            gapOrphProp(),
+            allOrphProp(1, 0, 0, 0, 0, 0, 1),
+            keepOneFull(1, 0, 0, 0, 0, 0, 1),
+            keepOneUser(1, 0, 0, 0, 0, 0, 1),
+            unmergedBcs(1, 0, 0, 0, 0, 0, 1),
+            betweenChkp(1, 0, 0, 0, 2, 0, 2),
+            btwnChkpUBC(1, 0, 0, 0, 2, 0, 2));
     }
 
     /**
@@ -1584,7 +1699,15 @@ public class VersionGarbageCollectorIT {
      */
     @Test
     public void lateWriteCreateChildTreeGC() throws Exception {
-        doLateWriteCreateChildrenGC(of("/a", "/a/b/c"), of("/a/b/c/d", 
"/a/b/c/d/e/f"), 3, "/d");
+        doLateWriteCreateChildrenGC(of("/a", "/a/b/c"), of("/a/b/c/d", 
"/a/b/c/d/e/f"), "/d",
+            gapOrphOnly(),
+            gapOrphProp(),
+            allOrphProp(3, 0, 0, 0, 0, 0, 3),
+            keepOneFull(3, 0, 0, 0, 0, 0, 3),
+            keepOneUser(3, 0, 0, 0, 0, 0, 3),
+            unmergedBcs(3, 0, 0, 0, 0, 0, 3),
+            betweenChkp(3, 0, 0, 0, 3/*4*/, 0, 4),
+            btwnChkpUBC(3, 0, 0, 0, 3/*4*/, 0, 4));
     }
 
     /**
@@ -1595,7 +1718,15 @@ public class VersionGarbageCollectorIT {
     public void lateWriteCreateChildGCLargePath() throws Exception {
         String longPath = repeat("p", PATH_LONG + 1);
         String path = "/grand/parent/" + longPath + "/longPathChild";
-        doLateWriteCreateChildrenGC(of("/grand/parent"), of(path), 2, "/d");
+        doLateWriteCreateChildrenGC(of("/grand/parent"), of(path), "/d",
+              gapOrphOnly(),
+              gapOrphProp(),
+              allOrphProp(2, 0, 0, 0, 0, 0, 2),
+              keepOneFull(2, 0, 0, 0, 0, 0, 2),
+              keepOneUser(2, 0, 0, 0, 0, 0, 2),
+              unmergedBcs(2, 0, 0, 0, 0, 0, 2),
+              betweenChkp(2, 0, 0, 0, 2/*3*/, 0, 3),
+              btwnChkpUBC(2, 0, 0, 0, 2/*3*/, 0, 3));
     }
 
     /**
@@ -1614,7 +1745,18 @@ public class VersionGarbageCollectorIT {
             commonOrphanParents.add(orphanParent);
             orphans.add(orphanParent + "/" + r.nextInt(24));
         }
-        doLateWriteCreateChildrenGC(nonOrphans, orphans, orphans.size() + 
commonOrphanParents.size(), "/d");
+        int expectedNumOrphanedDocs = orphans.size() + 
commonOrphanParents.size();
+        int expectedNumInternalPropRevsGCed = 4;//904=expectedNumOrphanedDocs 
+ 1;
+        int expectedNumDocsUpdatedGCed = expectedNumOrphanedDocs + 1;
+        doLateWriteCreateChildrenGC(nonOrphans, orphans, "/d",
+              gapOrphOnly(),
+              gapOrphProp(),
+              allOrphProp(expectedNumOrphanedDocs, 0, 0, 0, 0, 0, 
expectedNumOrphanedDocs),
+              keepOneFull(expectedNumOrphanedDocs, 0, 0, 0, 0, 0, 
expectedNumOrphanedDocs),
+              keepOneUser(expectedNumOrphanedDocs, 0, 0, 0, 0, 0, 
expectedNumOrphanedDocs),
+              unmergedBcs(expectedNumOrphanedDocs, 0, 0, 0, 0, 0, 
expectedNumOrphanedDocs),
+              betweenChkp(expectedNumOrphanedDocs, 0, 0, 0, 
expectedNumInternalPropRevsGCed, 0, expectedNumDocsUpdatedGCed),
+              btwnChkpUBC(expectedNumOrphanedDocs, 0, 0, 0, 
expectedNumInternalPropRevsGCed, 0, expectedNumDocsUpdatedGCed));
     }
 
     @Test
@@ -1637,10 +1779,14 @@ public class VersionGarbageCollectorIT {
         assertTrue(getChildeNodeState(store1, "/a/b/c/d", true).exists());
         // should be 3 as it should clean up the _deleted from /a/b, /a/b/c 
and /a/b/c/d
         assertStatsCountsEqual(stats,
-                noOldPropGc(0, 0, 0, 0, 0, 0, 0),
+                gapOrphOnly(),
+                gapOrphProp(),
+                allOrphProp(),
                 keepOneFull(0, 0, 0, 3, 3, 0, 3),
                 keepOneUser(0, 0, 0, 3, 0, 0, 3),
-                betweenChkp(0, 0, 0, 0, 0, 0, 0));
+                unmergedBcs(),
+                betweenChkp(0, 0, 0, 0, 1, 0, 1),
+                btwnChkpUBC(0, 0, 0, 0, 1, 0, 1));
     }
 
     /**
@@ -1703,10 +1849,24 @@ public class VersionGarbageCollectorIT {
         VersionGCStats stats = gc(store1.getVersionGarbageCollector(), 1, 
HOURS);
         assertNotNull(stats);
         // expected 2 updated (deletions): /a/b/c/d and /a/b/c/d/e
-        assertEquals(2, stats.updatedDetailedGCDocsCount);
-        assertEquals(2, stats.deletedDocGCCount);
+        assertStatsCountsEqual(stats,
+                gapOrphOnly(2, 0, 0, 0, 0, 0, 2),
+                gapOrphProp(2, 0, 0, 0, 0, 0, 2),
+                allOrphProp(2, 0, 0, 0, 0, 0, 2),
+                keepOneFull(2, 0, 0, 0, 0, 0, 2),
+                keepOneUser(2, 0, 0, 0, 0, 0, 2),
+                unmergedBcs(2, 0, 0, 0, 0, 0, 2),
+                betweenChkp(2, 0, 0, 0, 3, 0, 4),
+                btwnChkpUBC(2, 0, 0, 0, 3, 0, 4));
 
-        createNodes("/a/b/c/d/e");
+        if (isModeOneOf(DetailedGCMode.NONE)) {
+            // in these modes the inconsistency isn't cleaned up
+            // which means there will be a OakMerge0004 exception upon
+            // trying to create the node(s) again.
+            // hence we can't really do this in thsse modes
+        } else {
+            createNodes("/a/b/c/d/e");
+        }
     }
 
     @Ignore(value="this is a reminder to add bundling-detailedGC tests in 
general, plus some of those cases combined with OAK-10542")
@@ -1758,13 +1918,57 @@ public class VersionGarbageCollectorIT {
         // below is an example of how the different modes result in different 
cleanups
         // this might help us narrow down differences in the modes
         assertStatsCountsEqual(stats,
-                noOldPropGc(0, 2, 0, 0,  0, 0, 1),
+                gapOrphOnly(),
+                gapOrphProp(0, 2, 0, 0,  0, 0, 1),
+                allOrphProp(0, 2, 0, 0,  0, 0, 1),
                 keepOneFull(0, 2, 2, 3, 11, 0, 2),
                 keepOneUser(0, 2, 0, 3,  0, 0, 1),
-                betweenChkp(0, 2, 1, 2, 13, 3, 2));
+                unmergedBcs(0, 2, 1, 2, 12, 3, 2),
+                betweenChkp(0, 2, 0, 0,  1, 0, 2),
+                btwnChkpUBC(0, 2, 1, 2, 13, 3, 2));
+    }
+
+    static GCCounts gapOrphOnly() {
+        return new GCCounts(DetailedGCMode.GAP_ORPHANS);
+    }
+
+    static GCCounts gapOrphOnly(int deletedDocGCCount, int deletedPropsCount,
+            int deletedInternalPropsCount, int deletedPropRevsCount,
+            int deletedInternalPropRevsCount, int deletedUnmergedBCCount,
+            int updatedDetailedGCDocsCount) {
+        assertEquals(0, deletedInternalPropsCount);
+        assertEquals(0, deletedPropRevsCount);
+        assertEquals(0, deletedInternalPropRevsCount);
+        assertEquals(0, deletedUnmergedBCCount);
+        return new GCCounts(DetailedGCMode.GAP_ORPHANS, deletedDocGCCount,
+                deletedPropsCount, deletedInternalPropsCount, 
deletedPropRevsCount,
+                deletedInternalPropRevsCount, deletedUnmergedBCCount,
+                updatedDetailedGCDocsCount);
+    }
+
+    static GCCounts gapOrphProp() {
+        return new GCCounts(DetailedGCMode.GAP_ORPHANS_EMPTYPROPS);
+    }
+
+    static GCCounts gapOrphProp(int deletedDocGCCount, int deletedPropsCount,
+            int deletedInternalPropsCount, int deletedPropRevsCount,
+            int deletedInternalPropRevsCount, int deletedUnmergedBCCount,
+            int updatedDetailedGCDocsCount) {
+        assertEquals(0, deletedInternalPropsCount);
+        assertEquals(0, deletedPropRevsCount);
+        assertEquals(0, deletedInternalPropRevsCount);
+        assertEquals(0, deletedUnmergedBCCount);
+        return new GCCounts(DetailedGCMode.GAP_ORPHANS_EMPTYPROPS, 
deletedDocGCCount,
+                deletedPropsCount, deletedInternalPropsCount, 
deletedPropRevsCount,
+                deletedInternalPropRevsCount, deletedUnmergedBCCount,
+                updatedDetailedGCDocsCount);
     }
 
-    static GCCounts noOldPropGc(int deletedDocGCCount, int deletedPropsCount,
+    static GCCounts allOrphProp() {
+        return new GCCounts(DetailedGCMode.ALL_ORPHANS_EMPTYPROPS);
+    }
+
+    static GCCounts allOrphProp(int deletedDocGCCount, int deletedPropsCount,
             int deletedInternalPropsCount, int deletedPropRevsCount,
             int deletedInternalPropRevsCount, int deletedUnmergedBCCount,
             int updatedDetailedGCDocsCount) {
@@ -1772,37 +1976,77 @@ public class VersionGarbageCollectorIT {
         assertEquals(0, deletedPropRevsCount);
         assertEquals(0, deletedInternalPropRevsCount);
         assertEquals(0, deletedUnmergedBCCount);
-        return new GCCounts(RDGCType.NO_OLD_PROP_REV_GC, deletedDocGCCount,
+        return new GCCounts(DetailedGCMode.ALL_ORPHANS_EMPTYPROPS, 
deletedDocGCCount,
                 deletedPropsCount, deletedInternalPropsCount, 
deletedPropRevsCount,
                 deletedInternalPropRevsCount, deletedUnmergedBCCount,
                 updatedDetailedGCDocsCount);
     }
 
+    static GCCounts keepOneFull() {
+        return new 
GCCounts(DetailedGCMode.ORPHANS_EMPTYPROPS_KEEP_ONE_ALL_PROPS);
+    }
+
     static GCCounts keepOneFull(int deletedDocGCCount, int deletedPropsCount,
             int deletedInternalPropsCount, int deletedPropRevsCount,
             int deletedInternalPropRevsCount, int deletedUnmergedBCCount,
             int updatedDetailedGCDocsCount) {
-        return new GCCounts(RDGCType.KEEP_ONE_FULL_MODE, deletedDocGCCount,
+        return new 
GCCounts(DetailedGCMode.ORPHANS_EMPTYPROPS_KEEP_ONE_ALL_PROPS, 
deletedDocGCCount,
                 deletedPropsCount, deletedInternalPropsCount, 
deletedPropRevsCount,
                 deletedInternalPropRevsCount, deletedUnmergedBCCount,
                 updatedDetailedGCDocsCount);
     }
 
+    static GCCounts keepOneUser() {
+        return new 
GCCounts(DetailedGCMode.ORPHANS_EMPTYPROPS_KEEP_ONE_USER_PROPS);
+    }
+
     static GCCounts keepOneUser(int deletedDocGCCount, int deletedPropsCount,
             int deletedInternalPropsCount, int deletedPropRevsCount,
             int deletedInternalPropRevsCount, int deletedUnmergedBCCount,
             int updatedDetailedGCDocsCount) {
-        return new 
GCCounts(RDGCType.KEEP_ONE_CLEANUP_USER_PROPERTIES_ONLY_MODE,
+        return new 
GCCounts(DetailedGCMode.ORPHANS_EMPTYPROPS_KEEP_ONE_USER_PROPS,
                 deletedDocGCCount, deletedPropsCount, 
deletedInternalPropsCount,
                 deletedPropRevsCount, deletedInternalPropRevsCount,
                 deletedUnmergedBCCount, updatedDetailedGCDocsCount);
     }
 
+    static GCCounts unmergedBcs() {
+        return new GCCounts(DetailedGCMode.ORPHANS_EMPTYPROPS_UNMERGED_BC);
+    }
+
+    static GCCounts unmergedBcs(int deletedDocGCCount, int deletedPropsCount,
+            int deletedInternalPropsCount, int deletedPropRevsCount,
+            int deletedInternalPropRevsCount, int deletedUnmergedBCCount,
+            int updatedDetailedGCDocsCount) {
+        return new GCCounts(DetailedGCMode.ORPHANS_EMPTYPROPS_UNMERGED_BC,
+                deletedDocGCCount, deletedPropsCount, 
deletedInternalPropsCount,
+                deletedPropRevsCount, deletedInternalPropRevsCount,
+                deletedUnmergedBCCount, updatedDetailedGCDocsCount);
+    }
+
+    static GCCounts betweenChkp() {
+        return new 
GCCounts(DetailedGCMode.ORPHANS_EMPTYPROPS_BETWEEN_CHECKPOINTS_NO_UNMERGED_BC);
+    }
+
     static GCCounts betweenChkp(int deletedDocGCCount, int deletedPropsCount,
             int deletedInternalPropsCount, int deletedPropRevsCount,
             int deletedInternalPropRevsCount, int deletedUnmergedBCCount,
             int updatedDetailedGCDocsCount) {
-        return new 
GCCounts(RDGCType.OLDER_THAN_24H_AND_BETWEEN_CHECKPOINTS_MODE,
+        return new 
GCCounts(DetailedGCMode.ORPHANS_EMPTYPROPS_BETWEEN_CHECKPOINTS_NO_UNMERGED_BC,
+                deletedDocGCCount, deletedPropsCount, 
deletedInternalPropsCount,
+                deletedPropRevsCount, deletedInternalPropRevsCount,
+                deletedUnmergedBCCount, updatedDetailedGCDocsCount);
+    }
+
+    static GCCounts btwnChkpUBC() {
+        return new 
GCCounts(DetailedGCMode.ORPHANS_EMPTYPROPS_BETWEEN_CHECKPOINTS_WITH_UNMERGED_BC);
+    }
+
+    static GCCounts btwnChkpUBC(int deletedDocGCCount, int deletedPropsCount,
+            int deletedInternalPropsCount, int deletedPropRevsCount,
+            int deletedInternalPropRevsCount, int deletedUnmergedBCCount,
+            int updatedDetailedGCDocsCount) {
+        return new 
GCCounts(DetailedGCMode.ORPHANS_EMPTYPROPS_BETWEEN_CHECKPOINTS_WITH_UNMERGED_BC,
                 deletedDocGCCount, deletedPropsCount, 
deletedInternalPropsCount,
                 deletedPropRevsCount, deletedInternalPropRevsCount,
                 deletedUnmergedBCCount, updatedDetailedGCDocsCount);
@@ -1854,10 +2098,14 @@ public class VersionGarbageCollectorIT {
         clock.waitUntil(getCurrentTimestamp() + maxAgeMillis + 1);
         VersionGCStats stats = gc(gc, maxAgeHours, HOURS);
         assertStatsCountsEqual(stats,
-                noOldPropGc(0, 0, 0,  0, 0, 0, 0),
+                gapOrphOnly(),
+                gapOrphProp(),
+                allOrphProp(),
                 keepOneFull(0, 0, 0, 11, 0, 0, 1),
                 keepOneUser(0, 0, 0, 11, 0, 0, 1),
-                betweenChkp(0, 0, 0, 0, 0, 0, 0));
+                unmergedBcs(),
+                betweenChkp(0, 0, 0, 0, 1, 0, 1),
+                btwnChkpUBC(0, 0, 0, 0, 1, 0, 1));
 
         NodeState x = store1.getRoot().getChildNode("x");
         assertTrue(x.exists());
@@ -1870,8 +2118,12 @@ public class VersionGarbageCollectorIT {
 
         NodeDocument doc = store1.getDocumentStore().find(NODES, "1:/x", -1);
         assertNotNull(doc);
-        if (VersionGarbageCollector.getRevisionDetailedGcType() == 
RDGCType.OLDER_THAN_24H_AND_BETWEEN_CHECKPOINTS_MODE
-                || VersionGarbageCollector.getRevisionDetailedGcType() == 
RDGCType.NO_OLD_PROP_REV_GC) {
+        if (VersionGarbageCollector.getDetailedGcMode() == 
DetailedGCMode.ORPHANS_EMPTYPROPS_BETWEEN_CHECKPOINTS_WITH_UNMERGED_BC
+                || VersionGarbageCollector.getDetailedGcMode() == 
DetailedGCMode.NONE || VersionGarbageCollector.getDetailedGcMode() == 
DetailedGCMode.GAP_ORPHANS
+                || VersionGarbageCollector.getDetailedGcMode() == 
DetailedGCMode.GAP_ORPHANS_EMPTYPROPS
+                || VersionGarbageCollector.getDetailedGcMode() == 
DetailedGCMode.ALL_ORPHANS_EMPTYPROPS
+                || VersionGarbageCollector.getDetailedGcMode() == 
DetailedGCMode.ORPHANS_EMPTYPROPS_UNMERGED_BC
+                || VersionGarbageCollector.getDetailedGcMode() == 
DetailedGCMode.ORPHANS_EMPTYPROPS_BETWEEN_CHECKPOINTS_NO_UNMERGED_BC) {
             // this mode doesn't currently delete all revisions,
             // thus would fail below assert.
             return;
@@ -1912,10 +2164,14 @@ public class VersionGarbageCollectorIT {
 
         VersionGCStats stats = gc(gc, maxAge*2, HOURS);
         assertStatsCountsEqual(stats,
-                noOldPropGc(0, 1, 0, 0, 0, 0, 1),
+                gapOrphOnly(),
+                gapOrphProp(0, 1, 0, 0, 0, 0, 1),
+                allOrphProp(0, 1, 0, 0, 0, 0, 1),
                 keepOneFull(0, 1, 0, 0, 0, 0, 1),
                 keepOneUser(0, 1, 0, 0, 0, 0, 1),
-                betweenChkp(0, 1, 0, 0, 0, 0, 1));
+                unmergedBcs(0, 1, 0, 0, 0, 0, 1),
+                betweenChkp(0, 1, 0, 0, 1, 0, 2),
+                btwnChkpUBC(0, 1, 0, 0, 1, 0, 2));
         assertEquals(MIN_ID_VALUE, stats.oldestModifiedDocId);
 
         // 4. Save values of detailedGC settings collection fields
@@ -1941,10 +2197,15 @@ public class VersionGarbageCollectorIT {
         final String oldestModifiedDryRunDocId = stats.oldestModifiedDocId;
         final long oldestModifiedDocDryRunTimeStamp = 
stats.oldestModifiedDocTimeStamp;
 
-        assertStatsCountsEqual(stats, noOldPropGc(0, 1, 0, 0, 0, 0, 1),
+        assertStatsCountsEqual(stats,
+                gapOrphOnly(),
+                gapOrphProp(0, 1, 0, 0, 0, 0, 1),
+                allOrphProp(0, 1, 0, 0, 0, 0, 1),
                 keepOneFull(0, 1, 0, 0, 0, 0, 1),
                 keepOneUser(0, 1, 0, 0, 0, 0, 1),
-                betweenChkp(0, 1, 0, 0, 0, 0, 1));
+                unmergedBcs(0, 1, 0, 0, 0, 0, 1),
+                betweenChkp(0, 1, 0, 0, 1, 0, 2),
+                btwnChkpUBC(0, 1, 0, 0, 1, 0, 2));
         assertEquals(MIN_ID_VALUE, stats.oldestModifiedDocId);
         assertTrue(stats.detailedGCDryRunMode);
 
@@ -1989,10 +2250,15 @@ public class VersionGarbageCollectorIT {
         clock.waitUntil(clock.getTime() + HOURS.toMillis(2));
         // clean everything older than one hour
         VersionGCStats stats = gc(gc, 1, HOURS);
-        assertStatsCountsEqual(stats, noOldPropGc(0, 3, 0, 0, 0, 0, 2),
-                keepOneFull(0, 3, 2, 1, 17, 0, 3),
+        assertStatsCountsEqual(stats,
+                gapOrphOnly(),
+                gapOrphProp(0, 3, 0, 0, 0, 0, 2),
+                allOrphProp(0, 3, 0, 0, 0, 0, 2),
+                keepOneFull(0, 3, 2, 1,17, 0, 3),
                 keepOneUser(0, 3, 0, 1, 0, 0, 2),
-                betweenChkp(0, 3, 2, 1, 18, 4, 3));
+                unmergedBcs(0, 3, 2, 1,15, 4, 3),
+                betweenChkp(0, 3, 0, 0, 1, 0, 3),
+                btwnChkpUBC(0, 3, 2, 1,16, 4, 3));
         assertTrue(stats.detailedGCDryRunMode);
 
         assertBranchRevisionNotRemovedFromAllDocuments(store1, br1);
@@ -2035,10 +2301,14 @@ public class VersionGarbageCollectorIT {
         assertFalse(store1.getRoot().getChildNode("bar").hasProperty("prop"));
         assertNotNull(stats);
         assertStatsCountsEqual(stats,
-                noOldPropGc(0, 1, 0, 0, 0, 0, 1),
+                gapOrphOnly(),
+                gapOrphProp(0, 1, 0, 0, 0, 0, 1),
+                allOrphProp(0, 1, 0, 0, 0, 0, 1),
                 keepOneFull(0, 1, 0, 0, 0, 0, 1),
                 keepOneUser(0, 1, 0, 0, 0, 0, 1),
-                betweenChkp(0, 1, 0, 0, 0, 0, 1));
+                unmergedBcs(0, 1, 0, 0, 0, 0, 1),
+                betweenChkp(0, 1, 0, 0, 1, 0, 2),
+                btwnChkpUBC(0, 1, 0, 0, 1, 0, 2));
         assertDocumentsExist(of("/bar"));
     }
 
@@ -2076,10 +2346,14 @@ public class VersionGarbageCollectorIT {
         // we should still be seeing the garbage from late write and
         // thus it will be collected.
         assertStatsCountsEqual(stats,
-                noOldPropGc(0, 1, 0, 0, 0, 0, 1),
+                gapOrphOnly(),
+                gapOrphProp(0, 1, 0, 0, 0, 0, 1),
+                allOrphProp(0, 1, 0, 0, 0, 0, 1),
                 keepOneFull(0, 1, 0, 0, 0, 0, 1),
                 keepOneUser(0, 1, 0, 0, 0, 0, 1),
-                betweenChkp(0, 1, 0, 0, 0, 0, 1));
+                unmergedBcs(0, 1, 0, 0, 0, 0, 1),
+                betweenChkp(0, 1, 0, 0, 2, 0, 2),
+                btwnChkpUBC(0, 1, 0, 0, 2, 0, 2));
 
         assertDocumentsExist(of("/foo/bar/baz"));
     }
@@ -2119,10 +2393,14 @@ public class VersionGarbageCollectorIT {
         // deletedPropRevsCount : 2 prop-revs GCed : the original prop=value, 
plus the
         // removeProperty(prop) plus 1 _commitRoot entry
         assertStatsCountsEqual(stats,
-                noOldPropGc(0, 0, 0, 0, 0, 0, 0),
+                gapOrphOnly(),
+                gapOrphProp(),
+                allOrphProp(),
                 keepOneFull(0, 0, 0, 2, 0, 0, 1),
                 keepOneUser(0, 0, 0, 2, 0, 0, 1),
-                betweenChkp(0, 0, 0, 0, 0, 0, 0));
+                unmergedBcs(),
+                betweenChkp(0, 0, 0, 0, 2, 0, 1),
+                btwnChkpUBC(0, 0, 0, 0, 2, 0, 1));
         assertDocumentsExist(of("/bar"));
     }
 
@@ -2137,7 +2415,7 @@ public class VersionGarbageCollectorIT {
 
         // unrelated path should be such that the paths and unrelated path 
shouldn't have common parent
         // for e.g. if path is /bar & unrelated is /d then there common 
ancestor is "/" i.e. root.
-        lateWriteRemovePropertiesNodes(of("/bar"), null, "p");
+        lateWriteRemovePropertiesNodes(of("/bar"), null, false, "p");
 
         assertDocumentsExist(of("/bar"));
         assertPropertyNotExist("/bar", "p");
@@ -2151,10 +2429,14 @@ public class VersionGarbageCollectorIT {
         assertNotNull(stats);
         // 1 prop-rev removal : the late-write null
         assertStatsCountsEqual(stats,
-                noOldPropGc(0, 0, 0, 0, 0, 0, 0),
+                gapOrphOnly(),
+                gapOrphProp(),
+                allOrphProp(),
                 keepOneFull(0, 0, 0, 1, 0, 0, 1),
                 keepOneUser(0, 0, 0, 1, 0, 0, 1),
-                betweenChkp(0, 0, 0, 0, 0, 0, 0));
+                unmergedBcs(),
+                betweenChkp(0, 0, 0, 0, 1, 0, 1),
+                btwnChkpUBC(0, 0, 0, 0, 1, 0, 1));
         assertDocumentsExist(of("/bar"));
     }
 
@@ -2169,7 +2451,7 @@ public class VersionGarbageCollectorIT {
 
         // unrelated path should be such that the paths and unrelated path 
shouldn't have common parent
         // for e.g. if path is /bar & unrelated is /d then there common 
ancestor is "/" i.e. root.
-        lateWriteRemovePropertiesNodes(of("/foo/bar/baz"), "/a", "prop");
+        lateWriteRemovePropertiesNodes(of("/foo/bar/baz"), "/a", false, 
"prop");
 
         assertDocumentsExist(of("/foo/bar/baz"));
         assertPropertyNotExist("/foo/bar/baz", "prop");
@@ -2185,15 +2467,41 @@ public class VersionGarbageCollectorIT {
         // thus it will be collected.
         // removes 1 prop-rev : the late-write null
         assertStatsCountsEqual(stats,
-                noOldPropGc(0, 0, 0, 0, 0, 0, 0),
+                gapOrphOnly(),
+                gapOrphProp(),
+                allOrphProp(),
                 keepOneFull(0, 0, 0, 1, 0, 0, 1),
                 keepOneUser(0, 0, 0, 1, 0, 0, 1),
-                betweenChkp(0, 0, 0, 0, 0, 0, 0));
+                unmergedBcs(),
+                betweenChkp(0, 0, 0, 0, 2, 0, 1),
+                btwnChkpUBC(0, 0, 0, 0, 2, 0, 1));
         assertDocumentsExist(of("/foo/bar/baz"));
+        invalidateCaches(store1);
+        assertEquals("value", 
store1.getRoot().getChildNode("foo").getChildNode("bar")
+                
.getChildNode("baz").getProperty("prop").getValue(Type.STRING));
+    }
+
+    @SuppressWarnings("unchecked")
+    private void invalidateCaches(DocumentNodeStore dns) throws 
IllegalAccessException {
+        dns.invalidateNodeChildrenCache();
+        dns.getNodeCache().invalidateAll();
+        dns.getNodeChildrenCache().invalidateAll();
+        CachingCommitValueResolver cvr = (CachingCommitValueResolver) 
readField(dns, "commitValueResolver", true);
+        Cache<Revision, String> commitValueCache = (Cache<Revision, String>) 
readField( cvr, "commitValueCache", true);
+        commitValueCache.invalidateAll();
+    }
+
+    @Test
+    public void skipPropertyRemovedByLateWriteWithRelatedPath_normal() throws 
Exception {
+        doSkipPropertyRemovedByLateWriteWithRelatedPath(false);
     }
 
     @Test
-    public void skipPropertyRemovedByLateWriteWithRelatedPath() throws 
Exception {
+    public void skipPropertyRemovedByLateWriteWithRelatedPath_branch() throws 
Exception {
+        doSkipPropertyRemovedByLateWriteWithRelatedPath(true);
+    }
+
+    private void doSkipPropertyRemovedByLateWriteWithRelatedPath(boolean 
useBranchForUnrelatedPath) throws Exception {
         // create a node with property.
         assumeTrue(fixture.hasSinglePersistence());
         NodeBuilder nb = store1.getRoot().builder();
@@ -2203,7 +2511,7 @@ public class VersionGarbageCollectorIT {
 
         // unrelated path should be such that the paths and unrelated path 
shouldn't have common parent
         // for e.g. if path is /bar & unrelated is /d then there common 
ancestor is "/" i.e. root.
-        lateWriteRemovePropertiesNodes(of("/bar"), "/d", "prop");
+        lateWriteRemovePropertiesNodes(of("/bar"), "/d", 
useBranchForUnrelatedPath, "prop");
 
         assertDocumentsExist(of("/bar"));
         assertPropertyNotExist("/bar", "prop");
@@ -2214,15 +2522,25 @@ public class VersionGarbageCollectorIT {
         clock.waitUntil(clock.getTime() + HOURS.toMillis(2));
         // clean everything older than one hour
         VersionGCStats stats = gc(store1.getVersionGarbageCollector(), 1, 
HOURS);
+        assertTrue(store1.getRoot().getChildNode("d").exists());
+        invalidateCaches(store1);
+        assertTrue(store1.getRoot().getChildNode("d").exists());
         assertNotNull(stats);
         // we should be able to remove the property since we have updated an 
related path that has lead to an update
         // of common ancestor and this would make late write visible
         assertStatsCountsEqual(stats,
-                noOldPropGc(0, 1, 0, 0, 0, 0, 1),
+                gapOrphOnly(),
+                gapOrphProp(0, 1, 0, 0, 0, 0, 1),
+                allOrphProp(0, 1, 0, 0, 0, 0, 1),
                 keepOneFull(0, 1, 0, 0, 0, 0, 1),
                 keepOneUser(0, 1, 0, 0, 0, 0, 1),
-                betweenChkp(0, 1, 0, 0, 0, 0, 1));
-
+                unmergedBcs(0, 1, 0, 0, 0, 0, 1),
+                useBranchForUnrelatedPath ?
+                betweenChkp(0, 1, 0, 0, 1, 0, 2) :
+                betweenChkp(0, 1, 0, 0, 2, 0, 2),
+                useBranchForUnrelatedPath ?
+                btwnChkpUBC(0, 1, 0, 0, 1, 0, 2) :
+                btwnChkpUBC(0, 1, 0, 0, 2, 0, 2));
         assertDocumentsExist(of("/bar"));
     }
     // OAK-10676 END
@@ -3283,26 +3601,32 @@ public class VersionGarbageCollectorIT {
 
     private void lateWriteCreateNodes(Collection<String> orphanedPaths,
                                       String unrelatedPathOrNull) throws 
Exception {
-        lateWrite(orphanedPaths, TestUtils::createChild,
-                unrelatedPathOrNull, ADD_NODE_OPS, (ds, ops) -> 
ds.createOrUpdate(NODES, ops));
+        lateWrite(orphanedPaths, TestUtils::createChild, unrelatedPathOrNull, 
false,
+                ADD_NODE_OPS, (ds, ops) -> ds.createOrUpdate(NODES, ops));
     }
 
     private void lateWriteRemoveNodes(Collection<String> orphanedPaths,
                                       String unrelatedPathOrNull) throws 
Exception {
         lateWrite(orphanedPaths, (rb, path) -> childBuilder(rb, path).remove(),
-                unrelatedPathOrNull, REMOVE_NODE_OPS, (ds, ops) -> 
ds.createOrUpdate(NODES, ops));
+                unrelatedPathOrNull, false, REMOVE_NODE_OPS,
+                (ds, ops) -> ds.createOrUpdate(NODES, ops));
     }
 
     private void lateWriteAddPropertiesNodes(Collection<String> paths, String 
unrelatedPath, String propertyName,
                                              String propertyValue) throws 
Exception {
-        lateWrite(paths, (rb, path) -> childBuilder(rb, 
path).setProperty(propertyName, propertyValue), unrelatedPath,
-                setPropertyOps(propertyName), (ds, ops) -> 
ds.findAndUpdate(NODES, ops));
+        lateWrite(paths,
+                (rb, path) -> childBuilder(rb, path).setProperty(propertyName,
+                        propertyValue),
+                unrelatedPath, false, setPropertyOps(propertyName),
+                (ds, ops) -> ds.findAndUpdate(NODES, ops));
     }
 
-    private void lateWriteRemovePropertiesNodes(Collection<String> paths, 
String unrelatedPath, String propertyName)
+    private void lateWriteRemovePropertiesNodes(Collection<String> paths, 
String unrelatedPath, boolean bc4Unrelated, String propertyName)
             throws Exception {
-        lateWrite(paths, (rb, path) -> childBuilder(rb, 
path).removeProperty(propertyName), unrelatedPath,
-                setPropertyOps(propertyName), (ds, ops) -> 
ds.findAndUpdate(NODES, ops));
+        lateWrite(paths,
+                (rb, path) -> childBuilder(rb, 
path).removeProperty(propertyName),
+                unrelatedPath, bc4Unrelated, setPropertyOps(propertyName),
+                (ds, ops) -> ds.findAndUpdate(NODES, ops));
     }
 
     /**
@@ -3317,7 +3641,7 @@ public class VersionGarbageCollectorIT {
      * @param dataStoreConsumer persist late write changes to DocumentStore
      * @throws Exception in case of merge failure we throw exception
      */
-    private void lateWrite(Collection<String> paths, LateWriteChangesBuilder 
lateWriteChangesBuilder, String unrelatedPath,
+    private void lateWrite(Collection<String> paths, LateWriteChangesBuilder 
lateWriteChangesBuilder, String unrelatedPath, boolean bc4Unrealted,
                            Predicate<UpdateOp> filterPredicate, 
BiConsumer<DocumentStore, List<UpdateOp>> dataStoreConsumer) throws Exception {
         // this method requires store2 to be null as a prerequisite
         assertNull(store2);
@@ -3360,7 +3684,11 @@ public class VersionGarbageCollectorIT {
 
         // revive clusterId 2
         createSecondaryStore(LeaseCheckMode.LENIENT);
-        merge(store2, createChild(store2.getRoot().builder(), unrelatedPath));
+        if (bc4Unrealted) {
+            mergedBranchCommit(store2, nb -> createChild(nb, unrelatedPath));
+        } else {
+            merge(store2, createChild(store2.getRoot().builder(), 
unrelatedPath));
+        }
         store2.runBackgroundOperations();
         store2.dispose();
         store1.runBackgroundOperations();
@@ -3382,7 +3710,7 @@ public class VersionGarbageCollectorIT {
      *                                root to allow detecting late-writes as 
such
      */
     private void doLateWriteCreateChildrenGC(Collection<String> parents,
-                                             Collection<String> orphans, int 
expectedNumOrphanedDocs, String unrelatedPath)
+            Collection<String> orphans, String unrelatedPath, GCCounts... 
counts)
             throws Exception {
         assumeTrue(fixture.hasSinglePersistence());
         createNodes(parents);
@@ -3399,12 +3727,13 @@ public class VersionGarbageCollectorIT {
         // clean everything older than one hour
         VersionGCStats stats = gc(store1.getVersionGarbageCollector(), 1, 
HOURS);
         assertNotNull(stats);
-        assertEquals(expectedNumOrphanedDocs, stats.deletedDocGCCount);
-
+        assertStatsCountsEqual(stats, counts);
         assertDocumentsExist(parents);
         // and the main assert being: have those lateCreated (orphans) docs 
been deleted
         assertNodesDontExist(parents, orphans);
-        assertDocumentsDontExist(orphans);
+        if (!isModeOneOf(DetailedGCMode.NONE, DetailedGCMode.GAP_ORPHANS, 
DetailedGCMode.GAP_ORPHANS_EMPTYPROPS)) {
+            assertDocumentsDontExist(orphans);
+        }
     }
 
     private void assertNodesDontExist(Collection<String> existingNodes,

Reply via email to