Index: src/main/java/org/apache/jackrabbit/oak/util/SipHash.java =================================================================== --- src/main/java/org/apache/jackrabbit/oak/util/SipHash.java (revision 0) +++ src/main/java/org/apache/jackrabbit/oak/util/SipHash.java (working copy) @@ -0,0 +1,70 @@ +/* + * 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.util; + +/** + * An implementation of the SipHash-2-2 function, to prevent hash flooding. + */ +public class SipHash { + + private final long v0, v1, v2, v3; + + public SipHash(long seed) { + long k0 = seed; + long k1 = Long.rotateLeft(seed, 32); + v0 = k0 ^ 0x736f6d6570736575L; + v1 = k1 ^ 0x646f72616e646f6dL; + v2 = k0 ^ 0x6c7967656e657261L; + v3 = k1 ^ 0x7465646279746573L; + } + + public SipHash(SipHash parent, long m) { + long v0 = parent.v0; + long v1 = parent.v1; + long v2 = parent.v2; + long v3 = parent.v3; + int repeat = 2; + for (int i = 0; i < repeat; i++) { + v0 += v1; + v2 += v3; + v1 = Long.rotateLeft(v1, 13); + v3 = Long.rotateLeft(v3, 16); + v1 ^= v0; + v3 ^= v2; + v0 = Long.rotateLeft(v0, 32); + v2 += v1; + v0 += v3; + v1 = Long.rotateLeft(v1, 17); + v3 = Long.rotateLeft(v3, 21); + v1 ^= v2; + v3 ^= v0; + v2 = Long.rotateLeft(v2, 32); + } + v0 ^= m; + this.v0 = v0; + this.v1 = v1; + this.v2 = v2; + this.v3= v3; + } + + @Override + public int hashCode() { + long x = v0 ^ v1 ^ v2 ^ v3; + return (int) (x ^ (x >>> 16)); + } + +} Index: src/main/java/org/apache/jackrabbit/oak/plugins/index/counter/jmx/NodeCounter.java =================================================================== --- src/main/java/org/apache/jackrabbit/oak/plugins/index/counter/jmx/NodeCounter.java (revision 1733250) +++ src/main/java/org/apache/jackrabbit/oak/plugins/index/counter/jmx/NodeCounter.java (working copy) @@ -90,14 +90,27 @@ return syncCount; } } - PropertyState p = s.getProperty(NodeCounterEditor.COUNT_PROPERTY_NAME); + boolean hasCount; + long count; + PropertyState p = s.getProperty(NodeCounterEditor.COUNT_HASH_PROPERTY_NAME); if (p != null) { - long x = p.getValue(Type.LONG); + hasCount = true; + count = p.getValue(Type.LONG); + } else { + hasCount = false; + count = 0; + } + PropertyState pOld = s.getProperty(NodeCounterEditor.COUNT_PROPERTY_NAME); + if (pOld != null) { + hasCount = true; + count += pOld.getValue(Type.LONG); + } + if (hasCount) { if (max) { // in the node itself, we just add the resolution - x += ApproximateCounter.COUNT_RESOLUTION; + count += ApproximateCounter.COUNT_RESOLUTION; } - return x; + return count; } // check in the counter index (if it exists) s = child(root, @@ -118,8 +131,20 @@ } return x; } - p = s.getProperty(NodeCounterEditor.COUNT_PROPERTY_NAME); - if (p == null) { + p = s.getProperty(NodeCounterEditor.COUNT_HASH_PROPERTY_NAME); + if (p != null) { + hasCount = true; + count = p.getValue(Type.LONG); + } else { + hasCount = false; + count = 0; + } + pOld = s.getProperty(NodeCounterEditor.COUNT_PROPERTY_NAME); + if (pOld != null) { + hasCount = true; + count += pOld.getValue(Type.LONG); + } + if (!hasCount) { // we have an index, but no data long x = 0; if (max) { @@ -128,11 +153,10 @@ } return x; } - long x = p.getValue(Type.LONG); if (max) { - x += ApproximateCounter.COUNT_RESOLUTION; + count += ApproximateCounter.COUNT_RESOLUTION; } - return x; + return count; } @Override Index: src/main/java/org/apache/jackrabbit/oak/plugins/index/counter/NodeCounterEditorProvider.java =================================================================== --- src/main/java/org/apache/jackrabbit/oak/plugins/index/counter/NodeCounterEditorProvider.java (revision 1733250) +++ src/main/java/org/apache/jackrabbit/oak/plugins/index/counter/NodeCounterEditorProvider.java (working copy) @@ -41,6 +41,8 @@ public static final String RESOLUTION = "resolution"; + public static final String SEED = "seed"; + @Override @CheckForNull public Editor getIndexEditor(@Nonnull String type, @@ -49,14 +51,22 @@ if (!TYPE.equals(type)) { return null; } - NodeCounterRoot rootData = new NodeCounterRoot(); - rootData.callback = callback; - rootData.definition = definition; - rootData.root = root; + int resolution; PropertyState s = definition.getProperty(RESOLUTION); if (s != null) { - rootData.resolution = s.getValue(Type.LONG).intValue(); + resolution = s.getValue(Type.LONG).intValue(); + } else { + resolution = NodeCounterEditor.DEFAULT_RESOLUTION; } + long seed; + s = definition.getProperty(SEED); + if (s != null) { + seed = s.getValue(Type.LONG).intValue(); + } else { + seed = 0; + } + NodeCounterRoot rootData = new NodeCounterRoot( + resolution, seed, definition, root, callback); return new NodeCounterEditor(rootData, null, "/"); } Index: src/main/java/org/apache/jackrabbit/oak/plugins/index/counter/NodeCounterEditor.java =================================================================== --- src/main/java/org/apache/jackrabbit/oak/plugins/index/counter/NodeCounterEditor.java (revision 1733250) +++ src/main/java/org/apache/jackrabbit/oak/plugins/index/counter/NodeCounterEditor.java (working copy) @@ -27,7 +27,7 @@ import org.apache.jackrabbit.oak.spi.commit.Editor; import org.apache.jackrabbit.oak.spi.state.NodeBuilder; import org.apache.jackrabbit.oak.spi.state.NodeState; -import org.apache.jackrabbit.oak.util.ApproximateCounter; +import org.apache.jackrabbit.oak.util.SipHash; /** * An approximate descendant node counter mechanism. @@ -36,18 +36,41 @@ public static final String DATA_NODE_NAME = ":index"; public static final String COUNT_PROPERTY_NAME = ":count"; - public static final int DEFAULT_RESOLUTION = 1000; + public static final String COUNT_HASH_PROPERTY_NAME = ":cnt"; + public static final int DEFAULT_RESOLUTION = 10; private final NodeCounterRoot root; private final NodeCounterEditor parent; private final String name; private long countOffset; + private SipHash hash; public NodeCounterEditor(NodeCounterRoot root, NodeCounterEditor parent, String name) { this.parent = parent; this.root = root; this.name = name; } + + public NodeCounterEditor(NodeCounterRoot root, NodeCounterEditor parent, String name, SipHash hash) { + this.parent = parent; + this.root = root; + this.name = name; + this.hash = hash; + } + + private SipHash getHash() { + if (hash != null) { + return hash; + } + SipHash h; + if (parent == null) { + h = new SipHash(root.seed); + } else { + h = new SipHash(parent.getHash(), name.hashCode()); + } + this.hash = h; + return h; + } @Override public void enter(NodeState before, NodeState after) @@ -58,30 +81,22 @@ @Override public void leave(NodeState before, NodeState after) throws CommitFailedException { - long offset = ApproximateCounter.calculateOffset( - countOffset, root.resolution); - if (offset == 0) { + if (countOffset == 0) { return; } - // only read the value of the property if really needed NodeBuilder builder = getBuilder(); - PropertyState p = builder.getProperty(COUNT_PROPERTY_NAME); + PropertyState p = builder.getProperty(COUNT_HASH_PROPERTY_NAME); long count = p == null ? 0 : p.getValue(Type.LONG); - offset = ApproximateCounter.adjustOffset(count, - offset, root.resolution); - if (offset == 0) { - return; - } - count += offset; + count += countOffset; root.callback.indexUpdate(); - if (count == 0) { + if (count <= 0) { if (builder.getChildNodeCount(1) >= 0) { - builder.removeProperty(COUNT_PROPERTY_NAME); + builder.removeProperty(COUNT_HASH_PROPERTY_NAME); } else { builder.remove(); } } else { - builder.setProperty(COUNT_PROPERTY_NAME, count); + builder.setProperty(COUNT_HASH_PROPERTY_NAME, count); } } @@ -113,23 +128,29 @@ @CheckForNull public Editor childNodeChanged(String name, NodeState before, NodeState after) throws CommitFailedException { - return getChildIndexEditor(this, name); + return getChildIndexEditor(this, name, null); } @Override @CheckForNull public Editor childNodeAdded(String name, NodeState after) throws CommitFailedException { - count(1); - return getChildIndexEditor(this, name); + SipHash h = new SipHash(getHash(), name.hashCode()); + if ((h.hashCode() & root.bitMask) == 0) { + count(root.bitMask + 1); + } + return getChildIndexEditor(this, name, h); } @Override @CheckForNull public Editor childNodeDeleted(String name, NodeState before) throws CommitFailedException { - count(-1); - return getChildIndexEditor(this, name); + SipHash h = new SipHash(getHash(), name.hashCode()); + if ((h.hashCode() & root.bitMask) == 0) { + count(-(root.bitMask + 1)); + } + return getChildIndexEditor(this, name, h); } private void count(int offset) { @@ -140,15 +161,26 @@ } private Editor getChildIndexEditor(NodeCounterEditor nodeCounterEditor, - String name) { - return new NodeCounterEditor(root, this, name); + String name, SipHash hash) { + return new NodeCounterEditor(root, this, name, hash); } public static class NodeCounterRoot { - int resolution = DEFAULT_RESOLUTION; - NodeBuilder definition; - NodeState root; - IndexUpdateCallback callback; + final int resolution; + final long seed; + final int bitMask; + final NodeBuilder definition; + final NodeState root; + final IndexUpdateCallback callback; + + NodeCounterRoot(int resolution, long seed, NodeBuilder definition, NodeState root, IndexUpdateCallback callback) { + this.resolution = resolution; + this.seed = seed; + this.bitMask = (Integer.highestOneBit(resolution) * 2) - 1; + this.definition = definition; + this.root = root; + this.callback = callback; + } } }