Merge remote-tracking branch 'origin/trunk' into OAK-9881
diff --git a/oak-blob-plugins/src/test/java/org/apache/jackrabbit/oak/plugins/blob/datastore/DataStoreBlobStoreStatsTest.java b/oak-blob-plugins/src/test/java/org/apache/jackrabbit/oak/plugins/blob/datastore/DataStoreBlobStoreStatsTest.java
index fb9379f..f3df903 100644
--- a/oak-blob-plugins/src/test/java/org/apache/jackrabbit/oak/plugins/blob/datastore/DataStoreBlobStoreStatsTest.java
+++ b/oak-blob-plugins/src/test/java/org/apache/jackrabbit/oak/plugins/blob/datastore/DataStoreBlobStoreStatsTest.java
@@ -964,7 +964,7 @@
     }
 
     private static <T, R> R waitForMetric(Function<T, R> f, T input, R expected, R defaultValue) {
-        return waitForMetric(f, input, expected, defaultValue, 100, 1000);
+        return waitForMetric(f, input, expected, defaultValue, 100, 1500);
     }
 
     private static <T, R> R waitForMetric(Function<T, R> f, T input, R expected, R defaultValue, int intervalMilliseconds, int waitMilliseconds) {
@@ -988,7 +988,7 @@
     }
 
     private static <T> Long waitForNonzeroMetric(Function<T, Long> f, T input) {
-        return waitForNonzeroMetric(f, input, 100, 1000);
+        return waitForNonzeroMetric(f, input, 100, 1500);
     }
 
     private static <T> Long waitForNonzeroMetric(Function<T, Long> f, T input, int intervalMilliseconds, int waitMilliseconds) {
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/query/AbstractQueryTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/query/AbstractQueryTest.java
index f096b64..c1914ae 100644
--- a/oak-core/src/test/java/org/apache/jackrabbit/oak/query/AbstractQueryTest.java
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/query/AbstractQueryTest.java
@@ -29,6 +29,7 @@
 import java.text.ParseException;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -227,7 +228,14 @@
                     w.println(line);
                     line = line.substring("commit".length()).trim();
                     apply(root, line);
-                    root.commit();
+                    // This part of code is used by both lucene and elastic tests.
+                    // The index definitions in these tests don't have async property set
+                    // So lucene, in this case behaves in a sync manner. But the tests fail on Elastic,
+                    // since ES indexing is always async.
+                    // The below commit info map (sync-mode = rt) would make Elastic use RealTimeBulkProcessHandler.
+                    // This would make ES indexes also sync.
+                    // This will NOT have any impact on the lucene tests.
+                    root.commit(Collections.singletonMap("sync-mode", "rt"));
                 }
                 w.flush();
             }
diff --git a/oak-core/src/test/resources/org/apache/jackrabbit/oak/query/sql2.txt b/oak-core/src/test/resources/org/apache/jackrabbit/oak/query/sql2.txt
index 7168fe6..d7dfba9 100644
--- a/oak-core/src/test/resources/org/apache/jackrabbit/oak/query/sql2.txt
+++ b/oak-core/src/test/resources/org/apache/jackrabbit/oak/query/sql2.txt
@@ -407,21 +407,22 @@
 commit /testRoot + "test3": { "name": "Hallo" }
 commit /testRoot + "test4": { "name": "10%" }
 commit /testRoot + "test5": { "name": "10 percent" }
+commit /testRoot + "test6": { "name": "brave" }
 
 select a.name
   from [nt:base] as a
   where a.name is not null and isdescendantnode(a , '/testRoot') order by upper(a.name)
 10 percent
 10%
+brave
 Hallo
 hello
 World!
 
 select [jcr:path]
   from [nt:base]
-  where length(name) = 5
-/testRoot/test
-/testRoot/test3
+  where length(name) = 10
+/testRoot/test5
 
 select [jcr:path]
   from [nt:base]
@@ -440,8 +441,9 @@
 
 select [jcr:path]
   from [nt:base]
-  where name like '%o_%'
-/testRoot/test2
+  where name like '%e_%'
+/testRoot/test
+/testRoot/test5
 
 select [jcr:path]
   from [nt:base]
diff --git a/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexQueryCommonTest.java b/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexQueryCommonTest.java
index 93f8da3..84d9419 100644
--- a/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexQueryCommonTest.java
+++ b/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexQueryCommonTest.java
@@ -23,18 +23,11 @@
 import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeStore;
 import org.junit.After;
 import org.junit.Rule;
-import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
-
-import static org.junit.Assert.assertEquals;
-
 import java.io.File;
-import java.util.List;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 
-import javax.jcr.query.Query;
-
 /**
  * Tests the query engine using the default index implementation: the
  * {@link LuceneIndexProvider}
@@ -59,14 +52,6 @@
         executorService.shutdown();
     }
 
-    @Test
-    public void descendantTestWithIndexTagExplain() throws Exception {
-        List<String> result = executeQuery(
-                    "explain select [jcr:path] from [nt:base] where isdescendantnode('/test') option (index tag x)", Query.JCR_SQL2);
-        assertEquals("[[nt:base] as [nt:base] /* lucene:test-index(/oak:index/test-index) :ancestors:/test\n"
-                + "  where isdescendantnode([nt:base], [/test]) */]", result.toString());
-    }
-
     @Override
     public String getContainsValueForEqualityQuery_native() {
         return "+:ancestors:/test +propa:bar";
@@ -91,4 +76,10 @@
     public String getContainsValueForNotNullQuery_native() {
         return "+:ancestors:/test +propa:[* TO *]";
     }
+
+    @Override
+    public String getExplainValueForDescendantTestWithIndexTagExplain() {
+        return "[nt:base] as [nt:base] /* lucene:test-index(/oak:index/test-index) :ancestors:/test" +
+                " where isdescendantnode([nt:base], [/test]) */";
+    }
 }
diff --git a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/ElasticRequestHandler.java b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/ElasticRequestHandler.java
index ce9fff3..3e51a86 100644
--- a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/ElasticRequestHandler.java
+++ b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/ElasticRequestHandler.java
@@ -214,6 +214,7 @@
             return DEFAULT_SORTS;
         }
         Map<String, List<PropertyDefinition>> indexProperties = elasticIndexDefinition.getPropertiesByName();
+
         boolean hasTieBreaker = false;
         List<SortOptions> list = new ArrayList<>();
         for (QueryIndex.OrderEntry o : sortOrder) {
@@ -225,7 +226,12 @@
                 hasTieBreaker = true;
             } else if (JCR_SCORE.equals(sortPropertyName)) {
                 fieldName = "_score";
-            } else if (indexProperties.containsKey(sortPropertyName)) {
+            } else if (indexProperties.containsKey(sortPropertyName) || elasticIndexDefinition.getDefinedRules()
+                    .stream().anyMatch(rule -> rule.getConfig(sortPropertyName) != null)) {
+                // There are 2 conditions in this if statement -
+                // First one returns true if sortPropertyName is one of the defined indexed properties on the index
+                // Second condition returns true if sortPropertyName might not be explicitly defined but covered by a regex property
+                // in any of the defined index rules.
                 fieldName = elasticIndexDefinition.getElasticKeyword(sortPropertyName);
             } else {
                 LOG.warn("Unable to sort by {} for index {}", sortPropertyName, elasticIndexDefinition.getIndexName());
@@ -732,11 +738,11 @@
         return Query.of(q -> q.bool(bqBuilder.build()));
     }
 
-    private static Query nodeName(Filter.PropertyRestriction pr) {
+    private Query nodeName(Filter.PropertyRestriction pr) {
         String first = pr.first != null ? pr.first.getValue(Type.STRING) : null;
         if (pr.first != null && pr.first.equals(pr.last) && pr.firstIncluding && pr.lastIncluding) {
             // [property]=[value]
-            return Query.of(q -> q.term(t -> t.field(FieldNames.NODE_NAME).value(FieldValue.of(first))));
+            return Query.of(q -> q.term(t -> t.field(elasticIndexDefinition.getElasticKeyword(FieldNames.NODE_NAME)).value(FieldValue.of(first))));
         }
 
         if (pr.isLike) {
@@ -746,26 +752,32 @@
         throw new IllegalStateException("For nodeName queries only EQUALS and LIKE are supported " + pr);
     }
 
-    private static Query like(String name, String first) {first = first.replace('%', WildcardQuery.WILDCARD_STRING);
+    private Query like(String name, String first) {
+        first = first.replace('%', WildcardQuery.WILDCARD_STRING);
         first = first.replace('_', WildcardQuery.WILDCARD_CHAR);
 
         // If the query ends in a wildcard string (*) and has no other wildcard characters, use a prefix match query
         boolean hasSingleWildcardStringAtEnd = first.indexOf(WildcardQuery.WILDCARD_STRING) == first.length() - 1;
         boolean doesNotContainWildcardChar = first.indexOf(WildcardQuery.WILDCARD_CHAR) == -1;
 
+        // Non full text (Non analyzed) properties are keyword types in ES. For those field would be equal to name.
+        // Analyzed properties, however are of text type on which we can't perform wildcard or prefix queries so we use the keyword (sub) field
+        // by appending .keyword to the name here.
+        String field = elasticIndexDefinition.getElasticKeyword(name);
+
         if (hasSingleWildcardStringAtEnd && doesNotContainWildcardChar) {
             // remove trailing "*" for prefix query
             first = first.substring(0, first.length() - 1);
             if (JCR_PATH.equals(name)) {
                 return newPrefixPathQuery(first);
             } else {
-                return newPrefixQuery(name, first);
+                return newPrefixQuery(field, first);
             }
         } else {
             if (JCR_PATH.equals(name)) {
                 return newWildcardPathQuery(first);
             } else {
-                return newWildcardQuery(name, first);
+                return newWildcardQuery(field, first);
             }
         }
     }
diff --git a/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexQueryCommonTest.java b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexQueryCommonTest.java
index 4594893..5f05e98 100644
--- a/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexQueryCommonTest.java
+++ b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexQueryCommonTest.java
@@ -21,14 +21,6 @@
 import org.apache.jackrabbit.oak.plugins.index.IndexQueryCommonTest;
 import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeStore;
 import org.junit.ClassRule;
-import org.junit.Ignore;
-import org.junit.Test;
-
-import static org.junit.Assert.assertEquals;
-
-import java.util.List;
-
-import javax.jcr.query.Query;
 
 public class ElasticIndexQueryCommonTest extends IndexQueryCommonTest {
 
@@ -48,15 +40,6 @@
         return repositoryOptionsUtil.getOak().createContentRepository();
     }
 
-    @Test
-    public void descendantTestWithIndexTagExplain() {
-        List<String> result = executeQuery(
-                    "explain select [jcr:path] from [nt:base] where isdescendantnode('/test') option (index tag x)", Query.JCR_SQL2);
-        assertEquals("[[nt:base] as [nt:base] /* elasticsearch:test-index(/oak:index/test-index) "
-                + "{\"bool\":{\"filter\":[{\"term\":{\":ancestors\":{\"value\":\"/test\"}}}]}}\n"
-                + "  where isdescendantnode([nt:base], [/test]) */]", result.toString());
-    }
-
     @Override
     public String getContainsValueForEqualityQuery_native() {
         return "\"filter\":[{\"term\":{\":ancestors\":{\"value\":\"/test\"}}},{\"term\":{\"propa.keyword\":{\"value\":\"bar\"}}}]";
@@ -86,17 +69,10 @@
     }
 
     @Override
-    @Ignore("Failing on ES")
-    @Test
-    public void isChildNodeTest() throws Exception {
-        super.isChildNodeTest();
-    }
-
-    @Override
-    @Ignore("OAK-9858")
-    @Test
-    public void sql2FullText() throws Exception {
-        super.sql2FullText();
+    public String getExplainValueForDescendantTestWithIndexTagExplain() {
+        return "[nt:base] as [nt:base] /* elasticsearch:test-index(/oak:index/test-index) "
+                + "{\"bool\":{\"filter\":[{\"term\":{\":ancestors\":{\"value\":\"/test\"}}}]}}"
+                + " where isdescendantnode([nt:base], [/test]) */";
     }
 
 }
diff --git a/oak-search/src/test/java/org/apache/jackrabbit/oak/plugins/index/FunctionIndexCommonTest.java b/oak-search/src/test/java/org/apache/jackrabbit/oak/plugins/index/FunctionIndexCommonTest.java
index e56b718..e4c6f94 100644
--- a/oak-search/src/test/java/org/apache/jackrabbit/oak/plugins/index/FunctionIndexCommonTest.java
+++ b/oak-search/src/test/java/org/apache/jackrabbit/oak/plugins/index/FunctionIndexCommonTest.java
@@ -222,6 +222,36 @@
     }
 
     @Test
+    public void path() throws Exception {
+        Tree index = createIndex("pathIndex", Collections.<String>emptySet());
+        Tree func = index.addChild(FulltextIndexConstants.INDEX_RULES)
+                .addChild("nt:base")
+                .addChild(FulltextIndexConstants.PROP_NODE)
+                .addChild("pathFunction");
+        func.setProperty(FulltextIndexConstants.PROP_FUNCTION, "path()");
+
+        Tree test = root.getTree("/").addChild("test");
+        test.addChild("hello");
+        test.addChild("world");
+        test.addChild("hello world");
+        root.commit();
+        postCommitHook();
+
+        String query = "select [jcr:path] from [nt:base] where path() = '/test/world'";
+        assertThat(explain(query), containsString(getIndexProvider() + "pathIndex(/oak:index/pathIndex)"));
+        assertQuery(query, asList("/test/world"));
+
+        query = "select [jcr:path] from [nt:base] where path() like '%hell%'";
+        assertThat(explain(query), containsString(getIndexProvider() + "pathIndex(/oak:index/pathIndex)"));
+        assertQuery(query, asList("/test/hello", "/test/hello world"));
+
+        query = "select [jcr:path] from [nt:base] where path() like '%ll_'";
+        assertThat(explain(query), containsString(getIndexProvider() + "pathIndex(/oak:index/pathIndex)"));
+        assertQuery(query, asList("/test/hello"));
+
+    }
+
+    @Test
     public void testOrdering2() throws Exception {
         Tree index = root.getTree("/");
         Tree indexDefn = createTestIndexNode(index, indexOptions.getIndexType());
diff --git a/oak-search/src/test/java/org/apache/jackrabbit/oak/plugins/index/IndexQueryCommonTest.java b/oak-search/src/test/java/org/apache/jackrabbit/oak/plugins/index/IndexQueryCommonTest.java
index b8e0765..ed9c7ba 100644
--- a/oak-search/src/test/java/org/apache/jackrabbit/oak/plugins/index/IndexQueryCommonTest.java
+++ b/oak-search/src/test/java/org/apache/jackrabbit/oak/plugins/index/IndexQueryCommonTest.java
@@ -56,7 +56,6 @@
 
     @Override
     protected void createTestIndexNode() throws Exception {
-        setTraversalEnabled(false);
         Tree index = root.getTree("/");
         indexDefn = createTestIndexNode(index, indexOptions.getIndexType());
         TestUtil.useV2(indexDefn);
@@ -78,16 +77,27 @@
         Tree dateProp = TestUtil.enableForOrdered(props, "propDate");
         dateProp.setProperty(FulltextIndexConstants.PROP_TYPE, "Date");
 
+        // Note  - certain tests in this class like #sql2 test regex based like queries.
+        // And since all the tests here use this common full text index - please be careful while adding any new properties.
+        // For example - #sql2() tests with a query on length of name property.
+        // Since this is a fulltext index with a regex property that indexes everything, those property names are also indexed.
+        // So if we add any property with propName that has length equal to what that test expects - that will effectively break the #sql2() test (giving more results).
+        // Ideally one would see the test failing while adding new properties - but there have been cases where this test was ignored due to a different reason
+        // and adding a new property added more failure reasons.
+
+        // So just be careful while changing the test collateral/setup here.
+
         root.commit();
     }
 
+    // TODO : The below 3 tests - #sql1, #sq2 and #sql2FullText need refactoring.
+    //  These are huge tests with multiple queries running and verification happening in the end by comparing against results in an expected test file.
+    //  These could possibly be broken down into several smaller tests instead which would make debugging much easier.
     @Test
     public void sql1() throws Exception {
         test("sql1.txt");
     }
 
-    // TODO: Test failing on Lucene and ES
-    @Ignore("OAK-9858")
     @Test
     public void sql2() throws Exception {
         test("sql2.txt");
@@ -168,6 +178,26 @@
     }
 
     @Test
+    public void descendantTestWithIndexTagExplain() throws Exception {
+        Tree test = root.getTree("/").addChild("test");
+        test.addChild("a");
+        test.addChild("b");
+        root.commit();
+
+        String query = "explain select [jcr:path] from [nt:base] where isdescendantnode('/test') option (index tag x)";
+        assertEventually(getAssertionForExplain(query, Query.JCR_SQL2, getExplainValueForDescendantTestWithIndexTagExplain(), true));
+    }
+
+    // Check if this is a valid behaviour or not ?
+    // This was discovered when we removed setTraversalEnabled(false); from the test setup.
+    @Ignore("Index not picked even when using option tag if traversal cost is lower")
+    @Test
+    public void descendantTestWithIndexTagExplainWithNoData() {
+        String query = "explain select [jcr:path] from [nt:base] where isdescendantnode('/test') option (index tag x)";
+        assertEventually(getAssertionForExplain(query, Query.JCR_SQL2, getExplainValueForDescendantTestWithIndexTagExplain(), true));
+    }
+
+    @Test
     public void descendantTest2() throws Exception {
         Tree test = root.getTree("/").addChild("test");
         test.addChild("a").setProperty("name", asList("Hello", "World"), STRINGS);
@@ -184,8 +214,6 @@
         });
     }
 
-    // TODO: Test failing on Lucene and ES
-    @Ignore("OAK-9859")
     @Test
     public void isChildNodeTest() throws Exception {
         Tree tree = root.getTree("/");
@@ -573,7 +601,7 @@
 
         String query = "explain /jcr:root/test//*[propa!='bar']";
 
-        assertEventually(getAssertionForExplainContains(query, XPATH, getContainsValueForInequalityQuery_native()));
+        assertEventually(getAssertionForExplain(query, XPATH, getContainsValueForInequalityQuery_native(), false));
 
         String query2 = "/jcr:root/test//*[propa!='bar']";
 
@@ -592,7 +620,7 @@
 
         String query = "explain select * from [nt:base] as s where propa is not null and ISDESCENDANTNODE(s, '/test')";
 
-        assertEventually(getAssertionForExplainContains(query, SQL2, getContainsValueForNotNullQuery_native()));
+        assertEventually(getAssertionForExplain(query, SQL2, getContainsValueForNotNullQuery_native(), false));
 
         String query2 = "select * from [nt:base] as s where propa is not null and ISDESCENDANTNODE(s, '/test')";
 
@@ -612,7 +640,7 @@
 
         String query = "explain //*[propa!='bar']";
 
-        assertEventually(getAssertionForExplainContains(query, XPATH, getContainsValueForInequalityQueryWithoutAncestorFilter_native()));
+        assertEventually(getAssertionForExplain(query, XPATH, getContainsValueForInequalityQueryWithoutAncestorFilter_native(), false));
 
         String query2 = "//*[propa!='bar']";
 
@@ -632,7 +660,7 @@
         root.commit();
 
         String query = "explain /jcr:root/test//*[propa!='bar' and propb='world']";
-        assertEventually(getAssertionForExplainContains(query, XPATH, getContainsValueForEqualityInequalityCombined_native()));
+        assertEventually(getAssertionForExplain(query, XPATH, getContainsValueForEqualityInequalityCombined_native(), false));
 
         String query2 = "/jcr:root/test//*[propa!='bar' and propb='world']";
         // Expected - nodes with both properties defined and propb with value 'world' and propa with value not equal to bar should be returned
@@ -653,7 +681,7 @@
 
         String query = "explain /jcr:root/test//*[propa='bar']";
 
-        assertEventually(getAssertionForExplainContains(query, XPATH, getContainsValueForEqualityQuery_native()));
+        assertEventually(getAssertionForExplain(query, XPATH, getContainsValueForEqualityQuery_native(), false));
 
         String query2 = "/jcr:root/test//*[propa='bar']";
 
@@ -752,7 +780,9 @@
 
     public abstract String getContainsValueForNotNullQuery_native();
 
-    private Runnable getAssertionForExplainContains(String query, String language, String containValue) {
+    public abstract String getExplainValueForDescendantTestWithIndexTagExplain();
+
+    private Runnable getAssertionForExplain(String query, String language, String expected, boolean matchComplete) {
         return () -> {
             Result result = null;
             try {
@@ -761,7 +791,11 @@
                 fail(e.getMessage());
             }
             ResultRow row = result.getRows().iterator().next();
-            assertTrue(row.getValue("plan").toString().contains(containValue));
+            if (matchComplete) {
+                assertEquals(row.getValue("plan").toString(), expected);
+            } else {
+                assertTrue(row.getValue("plan").toString().contains(expected));
+            }
         };
     }
 
diff --git a/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/cache/NodeDocumentCache.java b/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/cache/NodeDocumentCache.java
index 95f147d..dc28e01 100644
--- a/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/cache/NodeDocumentCache.java
+++ b/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/cache/NodeDocumentCache.java
@@ -438,13 +438,23 @@
             Lock lock = locks.acquire(id);
             try {
                 NodeDocument cachedDoc = getIfPresent(id);
-                // if an old document is present in the cache, we can simply update it
                 if (cachedDoc != null && isNewer(cachedDoc, d)) {
+                    // if an old document is present in the cache, we can simply update it
                     putInternal(d, tracker);
-                // if the document hasn't been invalidated or added during the tracker lifetime,
-                // we can put it as well
                 } else if (cachedDoc == null && !tracker.mightBeenAffected(id)) {
+                    // if the document hasn't been invalidated or added during the tracker lifetime,
+                    // we can put it as well
                     putInternal(d, tracker);
+                } else {
+                    // in all other cases we don't know if the current document
+                    // is up-to-date. notify the other trackers and invalidate
+                    // the cache
+                    internalMarkChanged(id, tracker);
+                    if (isLeafPreviousDocId(id)) {
+                        prevDocumentsCache.invalidate(new StringValue(id));
+                    } else {
+                        nodeDocumentsCache.invalidate(new StringValue(id));
+                    }
                 }
             } finally {
                 lock.unlock();
diff --git a/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/mongo/MongoDocumentStore.java b/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/mongo/MongoDocumentStore.java
index f8c1095..8e69896 100644
--- a/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/mongo/MongoDocumentStore.java
+++ b/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/mongo/MongoDocumentStore.java
@@ -1411,7 +1411,9 @@
         List<String> resultKeys = new ArrayList<>(keys.size());
         CacheChangesTracker tracker = null;
         if (collection == Collection.NODES) {
-            tracker = nodesCache.registerTracker(keys);
+            // keys set is modified later. create a copy of the keys set
+            // owned by the cache changes tracker.
+            tracker = nodesCache.registerTracker(new HashSet<>(keys));
         }
         Throwable t;
         try {
diff --git a/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/ConcurrentPrefetchAndUpdateIT.java b/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/ConcurrentPrefetchAndUpdateIT.java
index b6cc9c3..a707a77 100755
--- a/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/ConcurrentPrefetchAndUpdateIT.java
+++ b/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/ConcurrentPrefetchAndUpdateIT.java
@@ -33,7 +33,6 @@
 import org.jetbrains.annotations.Nullable;
 import org.junit.After;
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Test;
 
 import static org.apache.jackrabbit.oak.plugins.document.Collection.NODES;
@@ -71,7 +70,6 @@
         System.clearProperty(DocumentNodeStore.SYS_PROP_PREFETCH);
     }
 
-    @Ignore("OAK-9850")
     @Test
     public void cacheConsistency() throws Exception {
         Revision r = newRevision();
diff --git a/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/cache/NodeDocumentCacheTest.java b/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/cache/NodeDocumentCacheTest.java
new file mode 100644
index 0000000..6cb22db
--- /dev/null
+++ b/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/cache/NodeDocumentCacheTest.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.jackrabbit.oak.plugins.document.cache;
+
+import org.apache.jackrabbit.oak.plugins.document.Document;
+import org.apache.jackrabbit.oak.plugins.document.DocumentStore;
+import org.apache.jackrabbit.oak.plugins.document.NodeDocument;
+import org.apache.jackrabbit.oak.plugins.document.locks.NodeDocumentLocks;
+import org.apache.jackrabbit.oak.plugins.document.locks.StripedNodeDocumentLocks;
+import org.apache.jackrabbit.oak.plugins.document.memory.MemoryDocumentStore;
+import org.junit.Before;
+import org.junit.Test;
+
+import static java.util.Collections.singleton;
+import static org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder.newDocumentNodeStoreBuilder;
+import static org.junit.Assert.assertEquals;
+
+public class NodeDocumentCacheTest {
+
+    private static final String ID = "some-id";
+
+    private final DocumentStore store = new MemoryDocumentStore();
+
+    private final NodeDocumentLocks locks = new StripedNodeDocumentLocks();
+
+    private NodeDocumentCache cache;
+
+    @Before
+    public void setup() {
+        cache = newDocumentNodeStoreBuilder()
+                .buildNodeDocumentCache(store, locks);
+    }
+
+    @Test
+    public void cacheConsistency() throws Exception {
+        NodeDocument current = createDocument(0L);
+        NodeDocument updated = createDocument(1L);
+
+        // an update operation starts and registers a tracker
+        CacheChangesTracker updateTracker = cache.registerTracker(singleton(ID));
+        // the document is invalidated. this informs the update tracker
+        cache.invalidate(ID);
+        // a query operation starts and registers a tracker
+        CacheChangesTracker queryTracker = cache.registerTracker(singleton(ID));
+        // the query operation is able to read the document before it is updated
+        // but then gets delayed.
+        // the update operation wants to put the document into the cache, but
+        // can't because its tracker was informed by the cache invalidation
+        cache.putNonConflictingDocs(updateTracker, singleton(updated));
+        // the query operation wants to put the outdated document into the
+        // cache. the cache must not accept this outdated document.
+        cache.putNonConflictingDocs(queryTracker, singleton(current));
+
+        assertEquals(updated.getModCount(), cache.get(ID, () -> updated).getModCount());
+    }
+
+    private NodeDocument createDocument(long modCount) {
+        NodeDocument doc = new NodeDocument(store, modCount);
+        doc.put(Document.ID, ID);
+        doc.put(Document.MOD_COUNT, modCount);
+        return doc;
+    }
+}
diff --git a/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/prefetch/CacheWarmingTest.java b/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/prefetch/CacheWarmingTest.java
index 99cc90f..d391c21 100644
--- a/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/prefetch/CacheWarmingTest.java
+++ b/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/prefetch/CacheWarmingTest.java
@@ -16,7 +16,10 @@
  */
 package org.apache.jackrabbit.oak.plugins.document.prefetch;
 
+import static org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore.SYS_PROP_PREFETCH;
 import static org.apache.jackrabbit.oak.plugins.document.util.Utils.getIdFromPath;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.lessThan;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
@@ -30,6 +33,7 @@
 import java.util.TreeSet;
 
 import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.commons.junit.TemporarySystemProperty;
 import org.apache.jackrabbit.oak.plugins.document.Collection;
 import org.apache.jackrabbit.oak.plugins.document.CountingDocumentStore;
 import org.apache.jackrabbit.oak.plugins.document.DocumentMK;
@@ -48,6 +52,7 @@
 import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
 import org.apache.jackrabbit.oak.stats.Clock;
 import org.jetbrains.annotations.Nullable;
+import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 
@@ -56,6 +61,9 @@
 public class CacheWarmingTest {
 
     @Rule
+    public TemporarySystemProperty systemProperties = new TemporarySystemProperty();
+
+    @Rule
     public DocumentMKBuilderProvider builderProvider = new DocumentMKBuilderProvider();
 
     @Rule
@@ -65,6 +73,11 @@
 
     private CountingMongoDatabase db;
 
+    @Before
+    public void enablePrefetch() {
+        System.setProperty(SYS_PROP_PREFETCH, "true");
+    }
+
     @Test
     public void noop1() {
         DocumentStore store = new MemoryDocumentStore();
@@ -164,7 +177,7 @@
         SortedSet<String> children = new TreeSet<String>();
         // create a bunch of nodes
         // make it 4 levels deep to avoid things like 'readChildren' to be able to use optimizations such as query()
-        for (int i = 0; i < 5*1024; i++) {
+        for (int i = 0; i < 4*1024; i++) {
             String name = "c" + i;
             children.add("/" + name + "/" + name + "/" + name + "/" + name);
             builder.child(name).child(name).child(name).child(name);
@@ -180,19 +193,25 @@
             store.getDocumentStore().invalidateCache();
             logAndReset("after invalidate", cds, sw);
         }
+        DocumentNodeState root = store.getRoot();
         if (prefetch) {
             final List<String> paths = new ArrayList<>(children);
             final java.util.Collection<String> withParents = withParents(paths);
             withParents.remove("/");
-            store.prefetch(withParents, null);
+            store.prefetch(withParents, root);
             logAndReset("after prefetch  ", cds, sw);
         }
         // read the children again
-        DocumentNodeState root = store.getRoot();
         for (String aChild : root.getChildNodeNames()) {
             assertTrue(root.getChildNode(aChild).getChildNode(aChild).getChildNode(aChild).getChildNode(aChild).exists());
         }
+        // raw find calls must be reasonably low with prefetch
+        int rawFindCalls = getRawFindCalls();
         logAndReset("read            ", cds, sw);
+        if (prefetch) {
+            // OAK-9883 - this assertion is not stable, disable for now
+            // assertThat(rawFindCalls, lessThan(10));
+        }
     }
 
     public static java.util.Collection<String> withParents(java.util.Collection<String> paths) {