You are viewing a plain text version of this content. The canonical link for it is here.
Posted to oak-commits@jackrabbit.apache.org by da...@apache.org on 2014/11/28 13:01:47 UTC

svn commit: r1642285 - in /jackrabbit/oak/trunk/oak-core/src: main/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/ test/java/org/apache/jackrabbit/oak/plugins/index/property/ test/java/org/apache/jackrabbit/oak/plugins/index/property/st...

Author: davide
Date: Fri Nov 28 12:01:46 2014
New Revision: 1642285

URL: http://svn.apache.org/r1642285
Log:
OAK-2077: Improve the resilence of the OrderedIndex for dangling links

tracked a warning when a dangling link is enountered and in case it
happens during an insert it attempts to clean it up.

Added:
    jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/Oak2077QueriesTest.java
Modified:
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStoreStrategy.java
    jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/BasicOrderedPropertyIndexQueryTest.java
    jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStorageStrategyTest.java

Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStoreStrategy.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStoreStrategy.java?rev=1642285&r1=1642284&r2=1642285&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStoreStrategy.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStoreStrategy.java Fri Nov 28 12:01:46 2014
@@ -17,10 +17,12 @@
 
 package org.apache.jackrabbit.oak.plugins.index.property.strategy;
 
+import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.collect.Iterators.singletonIterator;
 import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.ENTRY_COUNT_PROPERTY_NAME;
 import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.INDEX_CONTENT_NODE_NAME;
+import static org.apache.jackrabbit.oak.plugins.index.property.OrderedIndex.LANES;
 
 import java.io.UnsupportedEncodingException;
 import java.net.URLEncoder;
@@ -104,9 +106,15 @@ public class OrderedContentMirrorStoreSt
     private static final Random RND = new Random(System.currentTimeMillis());
     
     /**
+     * maximum number of attempt for potential recursive processes like seek() 
+     */
+    private static final int MAX_RETRIES = LANES+1;
+        
+    /**
      * the direction of the index.
      */
     private OrderDirection direction = OrderedIndex.DEFAULT_DIRECTION;
+    
 
     public OrderedContentMirrorStoreStrategy() {
         super();
@@ -146,7 +154,7 @@ public class OrderedContentMirrorStoreSt
         
         // we use the seek for seeking the right spot. The walkedLanes will have all our
         // predecessors
-        String entry = seek(index, condition, walked);
+        String entry = seek(index, condition, walked, 0, new FixingDanglingLinkCallback(index));
         if (LOG.isDebugEnabled()) {
             LOG.debug("fetchKeyNode() - entry: {} ", entry);
             printWalkedLanes("fetchKeyNode() - ", walked);
@@ -199,7 +207,9 @@ public class OrderedContentMirrorStoreSt
                     do {
                         entry = seek(index,
                             new PredicateEquals(key),
-                            walkedLanes
+                            walkedLanes,
+                            0,
+                            new LoggingDanglinLinkCallback()
                             );
                         lane0Next = getPropertyNext(index.getChildNode(walkedLanes[0]));
                         if (LOG.isDebugEnabled()) {
@@ -666,7 +676,9 @@ public class OrderedContentMirrorStoreSt
         private NodeState start;
         NodeState current;
         private NodeState index;
+        private NodeBuilder builder;
         String currentName;
+        private DanglingLinkCallback dlc = new LoggingDanglinLinkCallback();
 
         public FullIterator(NodeState index, NodeState start, boolean includeStart,
                             NodeState current) {
@@ -674,12 +686,19 @@ public class OrderedContentMirrorStoreSt
             this.start = start;
             this.current = current;
             this.index = index;
+            this.builder = new ReadOnlyBuilder(index);
         }
 
         @Override
         public boolean hasNext() {
+            String next = getPropertyNext(current);
             boolean hasNext = (includeStart && start.equals(current))
-                || (!includeStart && !Strings.isNullOrEmpty(getPropertyNext(current)));
+                || (!includeStart && !Strings.isNullOrEmpty(next)
+                    && ensureAndCleanNode(
+                                  builder, next, 
+                                  currentName == null ? "" : currentName, 
+                                  0,
+                                  dlc));
                         
             return hasNext;
         }
@@ -809,7 +828,7 @@ public class OrderedContentMirrorStoreSt
      * last argument
      */
     String seek(@Nonnull NodeBuilder index, @Nonnull Predicate<String> condition) {
-        return seek(index, condition, null);
+        return seek(index, condition, null, 0, new LoggingDanglinLinkCallback());
     }
     
     /**
@@ -823,11 +842,14 @@ public class OrderedContentMirrorStoreSt
      *            lane represented by the corresponding position in the array. <b>You have</b> to
      *            pass in an array already sized as {@link OrderedIndex#LANES} or an
      *            {@link IllegalArgumentException} will be raised
+     * @param retries the number of retries
      * @return the entry or null if not found
      */
     String seek(@Nonnull final NodeBuilder index,
                                @Nonnull final Predicate<String> condition,
-                               @Nullable final String[] walkedLanes) {
+                               @Nullable final String[] walkedLanes, 
+                               int retries,
+                               @Nullable DanglingLinkCallback callback) {
         boolean keepWalked = false;
         String searchfor = condition.getSearchFor();
         if (LOG.isDebugEnabled()) {
@@ -875,6 +897,9 @@ public class OrderedContentMirrorStoreSt
                     lane++;
                 } else {
                     if (condition.apply(nextkey)) {
+                        // this branch is used so far only for range queries.
+                        // while figuring out how to correctly reproduce the issue is less risky
+                        // to leave this untouched.
                         found = nextkey;
                     } else {
                         currentKey = nextkey;
@@ -901,7 +926,18 @@ public class OrderedContentMirrorStoreSt
                     lane--;
                 } else {
                     if (condition.apply(nextkey)) {
-                        found = nextkey;
+                        if (ensureAndCleanNode(index, nextkey, currentKey, lane, callback)) {
+                            found = nextkey;
+                        } else {
+                            if (retries < MAX_RETRIES) {
+                                return seek(index, condition, walkedLanes, ++retries, callback);
+                            } else {
+                                LOG.debug(
+                                    "Attempted a lookup and fix for {} times. Leaving it be and returning null",
+                                    retries);
+                                return null;
+                            }
+                        }
                     } else {
                         currentKey = nextkey;
                         currentNode = null;
@@ -919,6 +955,36 @@ public class OrderedContentMirrorStoreSt
     }
     
     /**
+     * ensure that the provided {@code next} actually exists as node. Attempt to clean it up
+     * otherwise.
+     * 
+     * @param index the {@code :index} node
+     * @param next the {@code :next} retrieved for the provided lane
+     * @param current the current node from which {@code :next} has been retrieved
+     * @param lane the lane on which we're looking into
+     * @return true if the node exists, false otherwise
+     */
+    private static boolean ensureAndCleanNode(@Nonnull final NodeBuilder index, 
+                                              @Nonnull final String next,
+                                              @Nonnull final String current,
+                                              final int lane,
+                                              @Nullable DanglingLinkCallback callback) {
+        checkNotNull(index);
+        checkNotNull(next);
+        checkNotNull(current);
+        checkArgument(lane < LANES && lane >= 0, "The lane must be between 0 and LANES");
+        
+        if (index.getChildNode(next).exists()) {
+            return true;
+        } else {
+            if (callback != null) {
+                callback.perform(current, next, lane);
+            }
+            return false;
+        }
+    }
+    
+    /**
      * predicate for evaluating 'key' equality across index 
      */
     static class PredicateEquals implements Predicate<String> {
@@ -1120,7 +1186,7 @@ public class OrderedContentMirrorStoreSt
      * @param value
      * @param lane
      */
-    static void setPropertyNext(@Nonnull final NodeBuilder node, 
+    public static void setPropertyNext(@Nonnull final NodeBuilder node, 
                                 final String value, final int lane) {
         if (node != null && value != null && lane >= 0 && lane < OrderedIndex.LANES) {
             PropertyState next = node.getProperty(NEXT);
@@ -1168,14 +1234,14 @@ public class OrderedContentMirrorStoreSt
     /**
      * short-cut for using NodeBuilder. See {@code getNext(NodeState)}
      */
-    static String getPropertyNext(@Nonnull final NodeBuilder node) {
+    public static String getPropertyNext(@Nonnull final NodeBuilder node) {
         return getPropertyNext(node, 0);
     }
 
     /**
      * short-cut for using NodeBuilder. See {@code getNext(NodeState)}
      */
-    static String getPropertyNext(@Nonnull final NodeBuilder node, final int lane) {
+    public static String getPropertyNext(@Nonnull final NodeBuilder node, final int lane) {
         checkNotNull(node);
         
         String next = "";
@@ -1217,7 +1283,7 @@ public class OrderedContentMirrorStoreSt
      * @param rnd the Random generator to be used for probability
      * @return the lane to be updated. 
      */
-    int getLane(@Nonnull final Random rnd) {
+    protected int getLane(@Nonnull final Random rnd) {
         final int maxLanes = OrderedIndex.LANES - 1;
         int lane = 0;
         
@@ -1227,4 +1293,64 @@ public class OrderedContentMirrorStoreSt
         
         return lane;
     }
+    
+    /**
+     * implementors of this interface will deal with the dangling link cases along the list
+     * (OAK-2077)
+     */
+    interface DanglingLinkCallback {
+        /**
+         * perform the required operation on the provided {@code current} node for the {@code next}
+         * value on {@code lane}
+         * 
+         * @param current the current node with the dangling link
+         * @param next the value pointing to the missing node
+         * @param lane the lane on which the link is on
+         */
+        void perform(String current, String next, int lane);
+    }
+    
+    /**
+     * implements a "Read-only" version for managing the dangling links which will simply track down
+     * in logs the presence of it
+     */
+    static class LoggingDanglinLinkCallback implements DanglingLinkCallback {
+        private boolean alreadyLogged;
+        
+        @Override
+        public void perform(@Nonnull final String current, 
+                            @Nonnull final String next, 
+                            int lane) {
+            checkNotNull(next);
+            checkNotNull(current);
+            checkArgument(lane < LANES && lane >= 0, "The lane must be between 0 and LANES");
+
+            if (!alreadyLogged) {
+                LOG.warn(
+                    "Dangling link to '{}' found on lane '{}' for key '{}'. Trying to clean it up. You may consider a reindex",
+                    new Object[] { next, lane, current });
+                alreadyLogged = true;
+            }
+        }
+    }
+    
+    static class FixingDanglingLinkCallback extends LoggingDanglinLinkCallback {
+        private final NodeBuilder indexContent;
+        
+        public FixingDanglingLinkCallback(@Nonnull final NodeBuilder indexContent) {
+            this.indexContent = checkNotNull(indexContent);
+        }
+
+        @Override
+        public void perform(String current, String next, int lane) {
+            super.perform(current, next, lane);
+            // as we're already pointing to nowhere it's safe to truncate here and avoid
+            // future errors. We'll fix all the lanes from slowest to fastest starting from the lane
+            // with the error. This should keep the list a bit more consistent with what is
+            // expected.
+            for (int l = lane; l < LANES; l++) {
+                setPropertyNext(indexContent.getChildNode(current), "", lane);                
+            }
+        }
+    }
 }
\ No newline at end of file

Modified: jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/BasicOrderedPropertyIndexQueryTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/BasicOrderedPropertyIndexQueryTest.java?rev=1642285&r1=1642284&r2=1642285&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/BasicOrderedPropertyIndexQueryTest.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/BasicOrderedPropertyIndexQueryTest.java Fri Nov 28 12:01:46 2014
@@ -20,6 +20,8 @@ import static junit.framework.Assert.ass
 import static junit.framework.Assert.assertTrue;
 import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE;
 import static org.apache.jackrabbit.JcrConstants.NT_UNSTRUCTURED;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.fail;
 
 import java.text.DecimalFormat;
 import java.text.NumberFormat;
@@ -66,35 +68,61 @@ public abstract class BasicOrderedProper
     protected static final String ISO_8601_2000 = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"; 
 
     /**
+     * same as {@link #generateOrderedValues(int, int, OrderDirection)} by passing {@code 0} as
+     * {@code offset}
+     * 
+     * @param amount
+     * @param direction
+     * @return
+     */
+    protected static List<String> generateOrderedValues(int amount, OrderDirection direction) {
+        return generateOrderedValues(amount, 0, direction);
+    }
+
+    /**
+     * <p>
      * generate a list of values to be used as ordered set. Will return something like
      * {@code value000, value001, value002, ...}
+     * </p>
      *
-     *
-     * @param amount
+     * @param amount the values to be generated
+     * @param offset move the current counter by this provided amount.
      * @param direction the direction of the sorting
      * @return a list of {@code amount} values ordered as specified by {@code direction}
      */
-    protected static List<String> generateOrderedValues(int amount, OrderDirection direction) {
+    protected static List<String> generateOrderedValues(int amount, int offset , OrderDirection direction) {
         if (amount > 1000) {
             throw new RuntimeException("amount cannot be greater than 1000");
         }
         List<String> values = new ArrayList<String>(amount);
-        NumberFormat nf = new DecimalFormat("000");
+        
 
         if (OrderDirection.DESC.equals(direction)) {
             for (int i = amount; i > 0; i--) {
-                values.add(String.format("value%s", String.valueOf(nf.format(i))));
+                values.add(formatNumber(i + offset));
             }
         } else {
             for (int i = 0; i < amount; i++) {
-                values.add(String.format("value%s", String.valueOf(nf.format(i))));
+                values.add(formatNumber(i + offset));
             }
         }
         return values;
     }
+    
+    /**
+     * formats the provided number for being used by the
+     * {@link #generateOrderedValues(int, OrderDirection)}
+     * 
+     * @param number
+     * @return something in the format {@code value000}
+     */
+    public static String formatNumber(int number) {
+        NumberFormat nf = new DecimalFormat("0000");
+        return String.format("value%s", String.valueOf(nf.format(number)));
+    }
 
     /**
-     * as {@code generateOrderedValues(int, OrderDirection)} by forcing OrderDirection.ASC
+     * as {@link #generateOrderedValues(int, OrderDirection)} by forcing {@link OrderDirection.ASC}
      *
      * @param amount
      * @return
@@ -121,9 +149,13 @@ public abstract class BasicOrderedProper
     }
 
     /**
+     * <p>
      * convenience method that adds a bunch of nodes in random order and return the order in which
-     * they should be presented by the OrderedIndex
-     *
+     * they should be presented by the OrderedIndex.
+     * </p>
+     * <p>
+     * The nodes will be created using the {@link #ORDERED_PROPERTY} as property for indexing
+     * </p>
      * @param values the values of the property that will be indexed
      * @param father the father under which add the nodes
      * @param direction the direction of the items to be added.
@@ -154,22 +186,27 @@ public abstract class BasicOrderedProper
     /**
      * assert the right order of the returned resultset
      *
-     * @param orderedSequence the right order in which the resultset should be returned
+     * @param expected the right order in which the resultset should be returned
      * @param resultset the resultset
      */
-    protected void assertRightOrder(@Nonnull final List<ValuePathTuple> orderedSequence,
+    protected void assertRightOrder(@Nonnull final List<ValuePathTuple> expected,
                                     @Nonnull final Iterator<? extends ResultRow> resultset) {
-        assertTrue("No results returned", resultset.hasNext());
-        int counter = 0;
-        while (resultset.hasNext() && counter < orderedSequence.size()) {
-            ResultRow row = resultset.next();
-            assertEquals(
-                String.format("Wrong path at the element '%d'", counter),
-                orderedSequence.get(counter).getPath(),
-                row.getPath()
-            );
-            counter++;
-        }
+        if (expected.isEmpty()) {
+            assertFalse("An empty resultset is expected but something has been returned.",
+                resultset.hasNext());
+        } else {
+            assertTrue("No results returned", resultset.hasNext());
+            int counter = 0;
+            while (resultset.hasNext() && counter < expected.size()) {
+                ResultRow row = resultset.next();
+                assertEquals(
+                    String.format("Wrong path at the element '%d'", counter),
+                    expected.get(counter).getPath(),
+                    row.getPath()
+                );
+                counter++;
+            }
+        }        
     }
 
     /**

Added: jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/Oak2077QueriesTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/Oak2077QueriesTest.java?rev=1642285&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/Oak2077QueriesTest.java (added)
+++ jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/Oak2077QueriesTest.java Fri Nov 28 12:01:46 2014
@@ -0,0 +1,657 @@
+/*
+ * 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.index.property;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.INDEX_CONTENT_NODE_NAME;
+import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.INDEX_DEFINITIONS_NAME;
+import static org.apache.jackrabbit.oak.plugins.index.property.OrderedIndex.OrderDirection.ASC;
+import static org.apache.jackrabbit.oak.plugins.index.property.OrderedIndex.OrderDirection.DESC;
+import static org.apache.jackrabbit.oak.plugins.index.property.strategy.OrderedContentMirrorStoreStrategy.START;
+import static org.apache.jackrabbit.oak.plugins.index.property.strategy.OrderedContentMirrorStoreStrategy.getPropertyNext;
+import static org.apache.jackrabbit.oak.plugins.index.property.strategy.OrderedContentMirrorStoreStrategy.setPropertyNext;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.List;
+import java.util.Random;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.jcr.NoSuchWorkspaceException;
+import javax.jcr.RepositoryException;
+import javax.security.auth.login.LoginException;
+
+import org.apache.jackrabbit.oak.Oak;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.api.ContentRepository;
+import org.apache.jackrabbit.oak.api.Result;
+import org.apache.jackrabbit.oak.api.Tree;
+import org.apache.jackrabbit.oak.plugins.index.IndexUpdateCallback;
+import org.apache.jackrabbit.oak.plugins.index.IndexUtils;
+import org.apache.jackrabbit.oak.plugins.index.property.OrderedIndex.OrderDirection;
+import org.apache.jackrabbit.oak.plugins.index.property.strategy.IndexStoreStrategy;
+import org.apache.jackrabbit.oak.plugins.index.property.strategy.OrderedContentMirrorStoreStrategy;
+import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeStore;
+import org.apache.jackrabbit.oak.plugins.nodetype.write.InitialContent;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.Editor;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.security.OpenSecurityProvider;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.apache.jackrabbit.oak.spi.state.NodeStore;
+import org.apache.jackrabbit.oak.util.NodeUtil;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.OutputStreamAppender;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+
+public class Oak2077QueriesTest extends BasicOrderedPropertyIndexQueryTest {
+    private static final LoggingTracker<ILoggingEvent> LOGGING_TRACKER;
+    private NodeStore nodestore;
+    private ContentRepository repository;
+
+    static {
+
+        LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
+        
+        PatternLayoutEncoder encoder = new PatternLayoutEncoder();
+        encoder.setContext(lc);
+        encoder.setPattern("%msg%n");
+        encoder.start();
+        
+        LOGGING_TRACKER = new LoggingTracker<ILoggingEvent>();
+        LOGGING_TRACKER.setContext(lc);
+        LOGGING_TRACKER.setEncoder(encoder);
+        LOGGING_TRACKER.start();
+
+        // adding the new appender to the root logger
+        ((ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME))
+            .addAppender(LOGGING_TRACKER);
+
+        //configuring the logging level to desired value
+        ((ch.qos.logback.classic.Logger) LoggerFactory.getLogger(LOGGING_TRACKER.getName()))
+            .setLevel(Level.WARN);
+    }
+    
+    // ------------------------------------------------------------------------ < utility classes >
+    private static class LoggingTracker<E> extends OutputStreamAppender<E> {
+        private ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        
+        @Override
+        public void start() {
+            setOutputStream(baos);
+            super.start();
+        }
+        
+        /**
+         * reset the inner OutputStream. 
+         */
+        public void reset() {
+            baos.reset();
+        }
+        
+        public BufferedReader toBufferedReader() {
+            return new BufferedReader(new StringReader(baos.toString()));
+        }
+        
+        public int countLinesTracked() throws IOException {
+            int lines = 0;
+            BufferedReader br = toBufferedReader();
+            while (br.readLine() != null) {
+                lines++;
+            }
+            return lines;
+        }
+
+        @Override
+        public String getName() {
+            return LoggingTracker.class.getName();
+        }
+    }
+    
+    /**
+     * used to return an instance of IndexEditor with a defined Random for a better reproducible
+     * unit testing
+     */
+    private class SeededOrderedPropertyIndexEditorProvider extends OrderedPropertyIndexEditorProvider {
+        private Random rnd = new Random(1);
+        
+        @Override
+        public Editor getIndexEditor(String type, NodeBuilder definition, NodeState root,
+                                     IndexUpdateCallback callback) throws CommitFailedException {
+            Editor editor = (TYPE.equals(type)) ? new SeededPropertyIndexEditor(definition, root,
+                callback, rnd) : null;
+            return editor; 
+        }
+    }
+
+    /**
+     * index editor that will return a content strategy with 
+     */
+    private class SeededPropertyIndexEditor extends OrderedPropertyIndexEditor {
+        private Random rnd;
+        
+        public SeededPropertyIndexEditor(NodeBuilder definition, NodeState root,
+                                         IndexUpdateCallback callback, Random rnd) {
+            super(definition, root, callback);
+            this.rnd = rnd;
+        }
+
+        public SeededPropertyIndexEditor(SeededPropertyIndexEditor parent, String name) {
+            super(parent, name);
+            this.rnd = parent.rnd;
+        }
+
+        @Override
+        IndexStoreStrategy getStrategy(boolean unique) {
+            SeededOrderedMirrorStore store = new SeededOrderedMirrorStore();
+            if (!OrderedIndex.DEFAULT_DIRECTION.equals(getDirection())) {
+                store = new SeededOrderedMirrorStore(DESC);
+            }
+            store.setRandom(rnd);
+            return store;
+        }
+
+        @Override
+        PropertyIndexEditor getChildIndexEditor(PropertyIndexEditor parent, String name) {
+            return new SeededPropertyIndexEditor(this, name);
+        }
+    }
+    
+    /**
+     * mocking class that makes use of the provided {@link Random} instance for generating the lanes
+     */
+    private class SeededOrderedMirrorStore extends OrderedContentMirrorStoreStrategy {
+        private Random rnd = new Random();
+        
+        public SeededOrderedMirrorStore() {
+            super();
+        }
+
+        public SeededOrderedMirrorStore(OrderDirection direction) {
+            super(direction);
+        }
+
+        @Override
+        public int getLane() {
+            return getLane(rnd);
+        }
+        
+        public void setRandom(Random rnd) {
+            this.rnd = rnd;
+        }
+    }
+    
+    /**
+     * enum used for injecting the filter condition in the {@code filter()}
+     */
+    private enum FilterCondition {
+        GREATER_THAN, GREATER_THEN_EQUAL, LESS_THAN
+    };
+
+    // ---------------------------------------------------------------------------------- < tests >
+    @Override
+    protected ContentRepository createRepository() {
+        nodestore = new MemoryNodeStore();
+        repository = new Oak(nodestore).with(new InitialContent())
+            .with(new OpenSecurityProvider())
+            .with(new SeededOrderedPropertyIndexEditorProvider())
+            .with(new OrderedPropertyIndexProvider())
+            .createContentRepository(); 
+        return repository;
+    }
+
+    @Override
+    protected void createTestIndexNode() throws Exception {
+        // leaving it empty. Prefer to create the index definition in each method
+    }
+    
+    private void defineIndex(@Nonnull final OrderDirection direction) 
+                            throws IllegalArgumentException, RepositoryException, CommitFailedException {
+        checkNotNull(direction);
+        
+        Tree index = root.getTree("/");
+        
+        // removing any previously defined index definition for a complete reset
+        index = index.getChild(INDEX_DEFINITIONS_NAME);
+        if (index.exists()) {
+            index = index.getChild(TEST_INDEX_NAME);
+            if (index.exists()) {
+                index.remove();
+            }
+        }
+        index = root.getTree("/");
+        
+        // ensuring we have a clear reset of the environment
+        assertFalse("the index definition should not be here yet",
+            index.getChild(INDEX_DEFINITIONS_NAME).getChild(TEST_INDEX_NAME).exists());
+        
+        IndexUtils.createIndexDefinition(
+            new NodeUtil(index.getChild(INDEX_DEFINITIONS_NAME)),
+            TEST_INDEX_NAME,
+            false,
+            new String[] { ORDERED_PROPERTY },
+            null,
+            OrderedIndex.TYPE,
+            ImmutableMap.of(
+                OrderedIndex.DIRECTION, direction.getDirection()
+            )
+        );
+        root.commit();
+    }
+
+    /**
+     * <p>
+     * reset the environment variables to be sure to use the latest root. {@code session, root, qe}
+     * <p>
+     * 
+     * @throws IOException
+     * @throws LoginException
+     * @throws NoSuchWorkspaceException
+     */
+    private void resetEnvVariables() throws IOException, LoginException, NoSuchWorkspaceException {
+        session.close();
+        session = repository.login(null, null);
+        root = session.getLatestRoot();
+        qe = root.getQueryEngine();
+    }
+    
+    /**
+     * create the test content by the provided attributes
+     * 
+     * @param numberOfNodes the number of nodes to be created
+     * @param offset if starting by 0 or by {@code offset}
+     * @param direction the direction of the value
+     * @return the list of ValiePathTuple for later assertions
+     * @throws CommitFailedException
+     */
+    private List<ValuePathTuple> createContent(final int numberOfNodes,
+                                                final int offset,
+                                               @Nonnull final OrderDirection direction) 
+                                                   throws CommitFailedException {
+        checkNotNull(direction);
+        
+        Tree content = root.getTree("/").addChild("content").addChild("nodes");
+        List<String> values = generateOrderedValues(numberOfNodes, offset, direction);
+        List<ValuePathTuple> nodes = addChildNodes(values, content, direction, STRING);
+        root.commit();
+        
+        return nodes;
+    }
+    
+    /**
+     * truncate the {@link AbstractQueryTest#TEST_INDEX_NAME} index at the 4th element of the
+     * provided lane returning the previous value
+     * 
+     * @param lane the desired lane. Must be 0 <= {@code lane} < {@link OrderedIndex#LANES}
+     * @param inexistent the derired value to be injected
+     * @return the value before the change
+     * @throws Exception
+     */
+    @Nullable 
+    private String truncate(final int lane, @Nonnull final String inexistent) throws Exception {
+        checkNotNull(inexistent);
+        checkArgument(lane >= 0 && lane < OrderedIndex.LANES);
+        
+        String previousValue;
+        NodeBuilder rootBuilder = nodestore.getRoot().builder();
+        NodeBuilder builder = rootBuilder.getChildNode(INDEX_DEFINITIONS_NAME);
+        builder = builder.getChildNode(TEST_INDEX_NAME);
+        builder = builder.getChildNode(INDEX_CONTENT_NODE_NAME);
+        
+        NodeBuilder truncated = builder.getChildNode(START);
+        String truncatedName;
+        
+        for (int i = 0; i < 4; i++) {
+            // changing the 4th element. No particular reasons on why the 4th.
+            truncatedName = getPropertyNext(truncated, lane);
+            truncated = builder.getChildNode(truncatedName);
+        }
+        previousValue = getPropertyNext(truncated, lane);
+        setPropertyNext(truncated, inexistent, lane);
+        
+        nodestore.merge(rootBuilder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        resetEnvVariables();
+        
+        return previousValue;
+    }
+    
+    private void assertLogAndQuery(@Nonnull final String statement,
+                                   @Nonnull final List<ValuePathTuple> expected) throws Exception {
+        LOGGING_TRACKER.reset();
+        Result result = executeQuery(statement, SQL2, null);
+        assertRightOrder(expected, result.getRows().iterator());
+        assertTrue("We expect at least 1 warning message to be tracked",
+            LOGGING_TRACKER.countLinesTracked() >= 1);
+    }
+
+    /**
+     * filter out the provided list for later assertions
+     * 
+     * @param nodes the original list to be filtered
+     * @param inexistent the previously injected inexistent node
+     * @param condition the condition applied in the query to assert. if {@code null} it will behave
+     *            as a {@code NOT NULL} query.
+     * @param whereCondition if {@condition} is provided CANNOT BE null. it's the where clause
+     *            provided in the query to assert.
+     * @return the filtered list to be expected
+     */
+    @Nonnull
+    private List<ValuePathTuple> filter(@Nonnull final List<ValuePathTuple> nodes,
+                                        @Nonnull final String inexistent,
+                                        @Nullable final FilterCondition condition,
+                                        @Nullable final String whereCondition) {
+        checkNotNull(nodes);
+        checkNotNull(inexistent);
+        checkArgument(condition != null ? whereCondition != null : true,
+            "if 'condition' is not null'whereCondition' MUST be provided");
+        
+        return Lists.newArrayList(Iterables.filter(nodes, new Predicate<ValuePathTuple>() {
+            boolean stopHere;
+
+            @Override
+            public boolean apply(ValuePathTuple input) {
+                if (!stopHere) {
+                    stopHere = inexistent.equals(input.getValue());
+                }
+                boolean filter = true;
+                if (condition != null) {
+                    switch (condition) {
+                    case GREATER_THAN:
+                        filter = input.getValue().compareTo(whereCondition) > 0;
+                        break;
+                    case GREATER_THEN_EQUAL:
+                        filter = input.getValue().compareTo(whereCondition) >= 0;
+                        break;
+                    case LESS_THAN:
+                        filter = input.getValue().compareTo(whereCondition) < 0;
+                        break;
+                    default:
+                        break;
+                    }
+                }
+                return !stopHere && filter;
+            }
+        }));
+    }
+    
+    @Test
+    public void queryNotNullAscending() throws Exception {
+        setTraversalEnabled(false);
+        final int numberOfNodes = 20;
+        final OrderDirection direction = ASC;
+        final String inexistent  = formatNumber(numberOfNodes + 1);
+        final String statement = "SELECT * FROM [nt:base] WHERE " + ORDERED_PROPERTY
+                                 + " IS NOT NULL";
+        defineIndex(direction);
+        
+        List<ValuePathTuple> nodes = createContent(numberOfNodes, 0, direction);
+                
+        // truncating the list on lane 0
+        truncate(0, inexistent);
+        
+        //filtering out the part that should not be returned by the resultset.
+        List<ValuePathTuple> expected = filter(nodes, inexistent, null, null);
+        
+        // pointing to a non-existent node in lane 0 we expect the result to be truncated
+        assertLogAndQuery(statement, expected);
+        
+        setTraversalEnabled(true);
+    }
+    
+    @Test
+    public void queryNotNullDescending() throws Exception {
+        setTraversalEnabled(false);
+        final int numberOfNodes = 20;
+        final OrderDirection direction = DESC; //changed
+        final String inexistent  = formatNumber(0); //changed
+        final String statement = "SELECT * FROM [nt:base] WHERE " + ORDERED_PROPERTY
+                                 + " IS NOT NULL";
+        defineIndex(direction);
+        
+        List<ValuePathTuple> nodes = createContent(numberOfNodes, 1, direction);
+        
+        // truncating the list on lane 0
+        truncate(0, inexistent);
+                
+        //filtering out the part that should not be returned by the resultset.
+        List<ValuePathTuple> expected = filter(nodes, inexistent, null, null);
+        
+        // pointing to a non-existent node in lane 0 we expect the result to be truncated
+        assertLogAndQuery(statement, expected);
+
+        // as the full iterable used in `property IS NOT NULL` cases walk the index on lane 0, any
+        // other lanes doesn't matter.
+        
+        setTraversalEnabled(true);
+    }
+
+    // As of OAK-2202 we don't use the skip list for returning a specific key item, so we're not
+    // affected by OAK-2077
+    // public void queryEqualsAscending() throws Exception {
+    // }
+    // public void queryEqualsDescending() {
+    // }
+
+    @Test
+    public void queryGreaterThanAscending() throws Exception {
+        setTraversalEnabled(false);
+        final int numberOfNodes = 20;
+        final OrderDirection direction = ASC;
+        final String inexistent  = formatNumber(numberOfNodes + 1);
+        // as 'values' will start from 0, we're excluding first entry(ies)
+        final String whereCondition = formatNumber(1);
+        final String statement = "SELECT * FROM [nt:base] WHERE " + ORDERED_PROPERTY
+                                 + " > '%s'";
+        defineIndex(direction);
+                
+        List<ValuePathTuple> nodes = createContent(numberOfNodes, 0, direction);
+        
+        // truncating the list on lane 0
+        truncate(0, inexistent);
+        
+        //filtering out the part that should not be returned by the resultset.
+        List<ValuePathTuple> expected = filter(nodes, inexistent, FilterCondition.GREATER_THAN,
+            whereCondition);
+        
+        // pointing to a non-existent node in lane 0 we expect the result to be truncated
+        assertLogAndQuery(String.format(statement, whereCondition), expected);
+        
+        setTraversalEnabled(true);
+    }
+
+    /*
+     * for sake of simplicity we check the just the second lane but it should be the same for all
+     * other higher ones.
+     */
+    @Test
+    public void queryGreaterThanAscendingLane1() throws Exception {
+        setTraversalEnabled(false);
+        final int numberOfNodes = 20;
+        final OrderDirection direction = ASC;
+        String inexistent  = formatNumber(numberOfNodes + 1);
+        String whereCondition;
+        final String statement = "SELECT * FROM [nt:base] WHERE " + ORDERED_PROPERTY
+                                 + " > '%s'";
+        defineIndex(direction);
+        
+        List<ValuePathTuple> nodes = createContent(numberOfNodes, 0, direction);
+                
+        whereCondition = truncate(1, inexistent);
+        
+        //filtering out the part that should not be returned by the resultset.
+        List<ValuePathTuple> expected = filter(nodes, inexistent, FilterCondition.GREATER_THAN,
+            whereCondition);
+
+        // no logging should be applied as the missing item does not match the seek condition
+        // we don't care about the logging then.
+        String st = String.format(statement, whereCondition);
+        Result result = executeQuery(st, SQL2, null);
+        assertRightOrder(expected, result.getRows().iterator());
+
+        setTraversalEnabled(true);
+    }
+
+    @Test
+    public void queryGreaterThenDescending() throws Exception {
+        setTraversalEnabled(false);
+        final int numberOfNodes = 20;
+        final int offset = 5;
+        final OrderDirection direction = DESC;
+        final String whereCondition = formatNumber(1);
+        final String inexistent  = formatNumber(3);
+        final String statement = "SELECT * FROM [nt:base] WHERE " + ORDERED_PROPERTY
+                                 + " > '%s'";
+        defineIndex(direction);
+        
+        List<ValuePathTuple> nodes = createContent(numberOfNodes, offset, direction);
+        
+        // truncating the list on lane 0
+        truncate(0, inexistent);
+        
+        //filtering out the part that should not be returned by the resultset.
+        List<ValuePathTuple> expected = filter(nodes, inexistent, FilterCondition.GREATER_THAN,
+            whereCondition);
+        
+        // pointing to a non-existent node in lane 0 we expect the result to be truncated
+        assertLogAndQuery(String.format(statement, whereCondition), expected);
+        
+        setTraversalEnabled(true);
+    }
+    
+    @Test
+    public void queryGreaterThanEqualAscending() throws Exception {
+        setTraversalEnabled(false);
+        final int numberOfNodes = 20;
+        final OrderDirection direction = ASC;
+        final String inexistent  = formatNumber(numberOfNodes + 1);
+        // as 'values' will start from 0, we're excluding first entry(ies)
+        final String whereCondition = formatNumber(1);
+        final String statement = "SELECT * FROM [nt:base] WHERE " + ORDERED_PROPERTY
+                                 + " >= '%s'";
+        defineIndex(direction);
+        
+        List<ValuePathTuple> nodes = createContent(numberOfNodes, 0, direction);
+        
+        truncate(0, inexistent);
+        
+        //filtering out the part that should not be returned by the resultset.
+        List<ValuePathTuple> expected = filter(nodes, inexistent,
+            FilterCondition.GREATER_THEN_EQUAL, whereCondition);
+        
+        // pointing to a non-existent node in lane 0 we expect the result to be truncated
+        assertLogAndQuery(String.format(statement, whereCondition), expected);
+        
+        setTraversalEnabled(true);
+    }
+    
+    @Test
+    public void queryGreaterThanEqualDescending() throws Exception {
+        setTraversalEnabled(false);
+        final int numberOfNodes = 20;
+        final int offset = 5;
+        final OrderDirection direction = DESC;
+        final String whereCondition = formatNumber(1);
+        final String inexistent  = formatNumber(3);
+        final String statement = "SELECT * FROM [nt:base] WHERE " + ORDERED_PROPERTY
+                                 + " >= '%s'";
+        defineIndex(direction);
+        
+        List<ValuePathTuple> nodes = createContent(numberOfNodes, offset, direction);
+        
+        // truncating the list on lane 0
+        truncate(0, inexistent);
+        
+        //filtering out the part that should not be returned by the resultset.
+        List<ValuePathTuple> expected = filter(nodes, inexistent,
+            FilterCondition.GREATER_THEN_EQUAL, whereCondition);
+        
+        // pointing to a non-existent node in lane 0 we expect the result to be truncated
+        assertLogAndQuery(String.format(statement, whereCondition), expected);
+        
+        setTraversalEnabled(true);
+    }
+    
+    @Test
+    public void queryLessThanAscending() throws Exception {
+        setTraversalEnabled(false);
+        final int numberOfNodes = 20;
+        final OrderDirection direction = ASC;
+        final String inexistent  = formatNumber(numberOfNodes + 1);
+        final String whereCondition = formatNumber(numberOfNodes + 2);
+        final String statement = "SELECT * FROM [nt:base] WHERE " + ORDERED_PROPERTY
+                                 + " < '%s'";
+        defineIndex(direction);
+        
+        List<ValuePathTuple> nodes = createContent(numberOfNodes, 0, direction);
+        
+        truncate(0, inexistent);
+        
+        //filtering out the part that should not be returned by the resultset.
+        List<ValuePathTuple> expected = filter(nodes, inexistent, FilterCondition.LESS_THAN,
+            whereCondition);
+        
+        // pointing to a non-existent node in lane 0 we expect the result to be truncated
+        assertLogAndQuery(String.format(statement, whereCondition), expected);
+        
+        setTraversalEnabled(true);
+    }
+    
+    @Test
+    public void queryLessThanDescending() throws Exception {
+        setTraversalEnabled(false);
+        final int numberOfNodes = 20;
+        final int offset = 5;
+        final OrderDirection direction = DESC;
+        final String whereCondition = formatNumber(1);
+        final String inexistent  = formatNumber(3);
+        final String statement = "SELECT * FROM [nt:base] WHERE " + ORDERED_PROPERTY
+                                 + " < '%s'";
+        defineIndex(direction);
+        
+        List<ValuePathTuple> nodes = createContent(numberOfNodes, offset, direction);
+        
+        truncate(0, inexistent);
+        
+        //filtering out the part that should not be returned by the resultset.
+        List<ValuePathTuple> expected = filter(nodes, inexistent, FilterCondition.LESS_THAN,
+            whereCondition);
+        
+        // pointing to a non-existent node in lane 0 we expect the result to be truncated
+        assertLogAndQuery(String.format(statement, whereCondition), expected);
+        
+        setTraversalEnabled(true);
+    }
+}

Modified: jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStorageStrategyTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStorageStrategyTest.java?rev=1642285&r1=1642284&r2=1642285&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStorageStrategyTest.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStorageStrategyTest.java Fri Nov 28 12:01:46 2014
@@ -17,9 +17,17 @@
 
 package org.apache.jackrabbit.oak.plugins.index.property.strategy;
 
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.collect.Sets.newHashSet;
+import static org.apache.jackrabbit.oak.plugins.index.property.OrderedIndex.LANES;
+import static org.apache.jackrabbit.oak.plugins.index.property.OrderedIndex.OrderDirection.DESC;
 import static org.apache.jackrabbit.oak.plugins.index.property.strategy.OrderedContentMirrorStoreStrategy.NEXT;
 import static org.apache.jackrabbit.oak.plugins.index.property.strategy.OrderedContentMirrorStoreStrategy.START;
+import static org.apache.jackrabbit.oak.plugins.index.property.strategy.OrderedContentMirrorStoreStrategy.getPropertyNext;
+import static org.apache.jackrabbit.oak.plugins.index.property.strategy.OrderedContentMirrorStoreStrategy.setPropertyNext;
+import static org.apache.jackrabbit.oak.plugins.index.property.strategy.OrderedContentMirrorStoreStrategy.FixingDanglingLinkCallback;
+import static org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState.EMPTY_NODE;
 import static org.easymock.EasyMock.createNiceMock;
 import static org.easymock.EasyMock.expect;
 import static org.easymock.EasyMock.replay;
@@ -47,6 +55,7 @@ import org.apache.jackrabbit.oak.plugins
 import org.apache.jackrabbit.oak.plugins.index.IndexUtils;
 import org.apache.jackrabbit.oak.plugins.index.property.OrderedIndex;
 import org.apache.jackrabbit.oak.plugins.index.property.OrderedIndex.OrderDirection;
+import org.apache.jackrabbit.oak.plugins.index.property.strategy.OrderedContentMirrorStoreStrategy.PredicateGreaterThan;
 import org.apache.jackrabbit.oak.plugins.index.property.strategy.OrderedContentMirrorStoreStrategy.PredicateLessThan;
 import org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState;
 import org.apache.jackrabbit.oak.query.ast.Operator;
@@ -56,6 +65,7 @@ import org.apache.jackrabbit.oak.spi.que
 import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry;
 import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
 import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.apache.jackrabbit.oak.spi.state.ReadOnlyBuilder;
 import org.junit.Test;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -3123,7 +3133,7 @@ public class OrderedContentMirrorStorage
         
         try {
             item = store.seek(builder,
-                new OrderedContentMirrorStoreStrategy.PredicateEquals(searchFor), wl);
+                new OrderedContentMirrorStoreStrategy.PredicateEquals(searchFor), wl, 0, null);
             fail("With a wrong size for the lane it should have raised an exception");
         } catch (IllegalArgumentException e) {
             // so far so good. It was expected
@@ -3138,7 +3148,7 @@ public class OrderedContentMirrorStorage
         entry = searchFor;
         wl = new String[OrderedIndex.LANES];
         item = store.seek(builder,
-            new OrderedContentMirrorStoreStrategy.PredicateEquals(searchFor), wl);
+            new OrderedContentMirrorStoreStrategy.PredicateEquals(searchFor), wl, 0, null);
         assertNotNull(wl);
         assertEquals(OrderedIndex.LANES, wl.length);
         assertEquals("Wrong lane", lane0, wl[0]);
@@ -3155,7 +3165,7 @@ public class OrderedContentMirrorStorage
         entry = searchFor;
         wl = new String[OrderedIndex.LANES];
         item = store.seek(builder,
-            new OrderedContentMirrorStoreStrategy.PredicateEquals(searchFor), wl);
+            new OrderedContentMirrorStoreStrategy.PredicateEquals(searchFor), wl, 0, null);
         assertNotNull(wl);
         assertEquals(OrderedIndex.LANES, wl.length);
         assertEquals("Wrong lane", lane0, wl[0]);
@@ -3172,7 +3182,7 @@ public class OrderedContentMirrorStorage
         entry = searchFor;
         wl = new String[OrderedIndex.LANES];
         item = store.seek(builder,
-            new OrderedContentMirrorStoreStrategy.PredicateEquals(searchFor), wl);
+            new OrderedContentMirrorStoreStrategy.PredicateEquals(searchFor), wl, 0, null);
         assertNotNull(wl);
         assertEquals(OrderedIndex.LANES, wl.length);
         assertEquals("Wrong lane", lane0, wl[0]);
@@ -3245,7 +3255,7 @@ public class OrderedContentMirrorStorage
         
         try {
             item = store.seek(builder,
-                new OrderedContentMirrorStoreStrategy.PredicateEquals(searchFor), wl);
+                new OrderedContentMirrorStoreStrategy.PredicateEquals(searchFor), wl, 0, null);
             fail("With a wrong size for the lane it should have raised an exception");
         } catch (IllegalArgumentException e) {
             // so far so good. It was expected
@@ -3260,7 +3270,7 @@ public class OrderedContentMirrorStorage
         entry = searchFor;
         wl = new String[OrderedIndex.LANES];
         item = store.seek(builder,
-            new OrderedContentMirrorStoreStrategy.PredicateEquals(searchFor), wl);
+            new OrderedContentMirrorStoreStrategy.PredicateEquals(searchFor), wl, 0, null);
         assertNotNull(wl);
         assertEquals(OrderedIndex.LANES, wl.length);
         assertEquals("Wrong lane", lane0, wl[0]);
@@ -3277,7 +3287,7 @@ public class OrderedContentMirrorStorage
         entry = searchFor;
         wl = new String[OrderedIndex.LANES];
         item = store.seek(builder,
-            new OrderedContentMirrorStoreStrategy.PredicateEquals(searchFor), wl);
+            new OrderedContentMirrorStoreStrategy.PredicateEquals(searchFor), wl, 0, null);
         assertNotNull(wl);
         assertEquals(OrderedIndex.LANES, wl.length);
         assertEquals("Wrong lane", lane0, wl[0]);
@@ -3294,7 +3304,7 @@ public class OrderedContentMirrorStorage
         entry = searchFor;
         wl = new String[OrderedIndex.LANES];
         item = store.seek(builder,
-            new OrderedContentMirrorStoreStrategy.PredicateEquals(searchFor), wl);
+            new OrderedContentMirrorStoreStrategy.PredicateEquals(searchFor), wl, 0, null);
         assertNotNull(wl);
         assertEquals(OrderedIndex.LANES, wl.length);
         assertEquals("Wrong lane", lane0, wl[0]);
@@ -3309,7 +3319,7 @@ public class OrderedContentMirrorStorage
      * 
      * @param index
      */
-    private static void printSkipList(NodeState index) {
+    public static void printSkipList(NodeState index) {
         final String marker = "->o-";
         final String filler = "----";
         StringBuffer sb = new StringBuffer();
@@ -3659,4 +3669,100 @@ public class OrderedContentMirrorStorage
         assertEquals("path/f", resultset.next());
         assertFalse("We should have not any results left", resultset.hasNext());
     }
+    
+    @Test
+    public void oak2077() {
+        NodeBuilder index;
+        MockOrderedContentMirrorStoreStrategy ascending = new MockOrderedContentMirrorStoreStrategy();
+        MockOrderedContentMirrorStoreStrategy descending = new MockOrderedContentMirrorStoreStrategy(DESC);
+        MockOrderedContentMirrorStoreStrategy strategy;
+        OrderedIndex.Predicate<String> condition;
+        String missingEntry, node;
+        
+        
+        // creating a dangling link on each lane one at time. 
+        for (int lane = 0; lane < LANES; lane++) {
+            
+            // ---------------------------------------------------< ascending, plain/inserts case >
+            missingEntry = KEYS[5];
+            strategy = ascending;
+            condition = new PredicateGreaterThan(missingEntry, true);
+            index = EMPTY_NODE.builder();
+            node = oak2077CreateStructure(index, lane, strategy, missingEntry);
+
+            assertOak2077(condition, strategy, index, lane, node);
+
+            // ------------------------------------------------- < descending, plain/inserts case >
+            missingEntry = KEYS[0];
+            strategy = descending;
+            index = EMPTY_NODE.builder();
+            condition = new PredicateLessThan(missingEntry, true);
+            node = oak2077CreateStructure(index, lane, strategy, missingEntry);
+
+            assertOak2077(condition, strategy, index, lane, node);
+        }
+    }
+
+    private static void assertOak2077(@Nonnull final OrderedIndex.Predicate<String> condition, 
+                                      @Nonnull final OrderedContentMirrorStoreStrategy strategy, 
+                                      @Nonnull NodeBuilder index, 
+                                      final int lane, 
+                                      @Nonnull final String node) {
+        
+        checkNotNull(condition);
+        checkNotNull(strategy);
+        checkNotNull(index);
+        checkArgument(lane >= 0 && lane < LANES);
+        checkNotNull(node);
+        
+        NodeState indexState = index.getNodeState();
+        String[] wl = new String[LANES];
+        String entry;
+
+        entry = strategy.seek(index, condition, wl, 0, new FixingDanglingLinkCallback(index));
+        assertNull("the seeked node does not exist and should have been null. lane: " + lane, entry);
+        assertEquals(
+            "As the index is a NodeBuilder we expect the entry to be fixed. lane: " + lane, "",
+            getPropertyNext(index.getChildNode(node), lane));
+
+        index = new ReadOnlyBuilder(indexState);
+        entry = strategy.seek(index, condition);
+        assertNull("the seeked node does not exist and should have been null. lane: " + lane, entry);
+    }
+    
+    /**
+     * <p>
+     * utility method to create the structure for the {@link #oak2077()} test.
+     * </p>
+     * <p>
+     * Create an index according to strategy with nodes from {@code 001} to {@code 004}.
+     * </p>
+     * 
+     * @param lane
+     * @param strategy
+     * @param missingEntry
+     * @return the node name with the wrong lane for testing on it later on.
+     */
+    private static String oak2077CreateStructure(@Nonnull final NodeBuilder index,
+                                                      final int lane, 
+                                                      @Nonnull final MockOrderedContentMirrorStoreStrategy strategy,
+                                                      @Nonnull final String missingEntry) {
+        checkNotNull(index);
+        checkNotNull(strategy);
+        checkArgument(lane >= 0 && lane < LANES);
+        checkNotNull(missingEntry);
+        
+        String node;
+        
+        strategy.setLane(0);
+        strategy.update(index, "/we/dont/care", EMPTY_KEY_SET, newHashSet(KEYS[1]));
+        strategy.update(index, "/we/dont/care", EMPTY_KEY_SET, newHashSet(KEYS[2]));
+        strategy.setLane(lane);
+        strategy.update(index, "/we/dont/care", EMPTY_KEY_SET, newHashSet(KEYS[3]));
+        strategy.update(index, "/we/dont/care", EMPTY_KEY_SET, newHashSet(KEYS[4]));
+        node = KEYS[3];
+        setPropertyNext(index.getChildNode(node), missingEntry, lane); 
+        
+        return node;
+    }
 }