blob: f92b3cb9c748c8efd3b192c5c62ce7d487055a62 [file] [log] [blame]
/*
* 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.counter;
import org.apache.jackrabbit.oak.api.CommitFailedException;
import org.apache.jackrabbit.oak.api.PropertyState;
import org.apache.jackrabbit.oak.api.Type;
import org.apache.jackrabbit.oak.commons.PathUtils;
import org.apache.jackrabbit.oak.plugins.index.IndexUpdateCallback;
import org.apache.jackrabbit.oak.plugins.index.counter.jmx.NodeCounter;
import org.apache.jackrabbit.oak.plugins.index.property.Multiplexers;
import org.apache.jackrabbit.oak.spi.commit.Editor;
import org.apache.jackrabbit.oak.spi.mount.Mount;
import org.apache.jackrabbit.oak.spi.mount.MountInfoProvider;
import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
import org.apache.jackrabbit.oak.spi.state.NodeState;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.Map;
/**
* An approximate descendant node counter mechanism.
*/
public class NodeCounterEditor implements Editor {
public static final String DATA_NODE_NAME = ":index";
// the property that is used with the "old" (pseudo-random number generator based) method
public static final String COUNT_PROPERTY_NAME = ":count";
// the property that is used with the "new" (hash of the path based) method
public static final String COUNT_HASH_PROPERTY_NAME = ":cnt";
public static final int DEFAULT_RESOLUTION = 1000;
private final NodeCounterRoot root;
private final NodeCounterEditor parent;
private final String name;
private final MountInfoProvider mountInfoProvider;
private final Map<Mount, Integer> countOffsets;
private final Mount currentMount;
private final boolean mountCanChange;
private SipHash hash;
NodeCounterEditor(NodeCounterRoot root, MountInfoProvider mountInfoProvider) {
this.root = root;
this.name = "/";
this.parent = null;
this.mountInfoProvider = mountInfoProvider;
this.currentMount = mountInfoProvider.getDefaultMount();
this.mountCanChange = true;
this.countOffsets = new HashMap<>();
}
private NodeCounterEditor(NodeCounterRoot root, NodeCounterEditor parent, String name, SipHash hash, MountInfoProvider mountInfoProvider) {
this.parent = parent;
this.root = root;
this.name = name;
this.hash = hash;
this.mountInfoProvider = mountInfoProvider;
this.countOffsets = new HashMap<>();
if (parent.mountCanChange) {
String path = getPath();
this.currentMount = mountInfoProvider.getMountByPath(path);
this.mountCanChange = currentMount.isDefault() && supportMounts(path);
} else {
this.currentMount = this.parent.currentMount;
this.mountCanChange = false;
}
}
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)
throws CommitFailedException {
// nothing to do
}
@Override
public void leave(NodeState before, NodeState after)
throws CommitFailedException {
if (NodeCounter.COUNT_HASH) {
leaveNew(before, after);
return;
}
leaveOld(before, after);
}
private void leaveOld(NodeState before, NodeState after)
throws CommitFailedException {
if (countOffsets.isEmpty()) {
return;
}
boolean updated = false;
for (Map.Entry<Mount, Integer> e : countOffsets.entrySet()) {
long offset = ApproximateCounter.calculateOffset(e.getValue(), root.resolution);
if (offset == 0) {
continue;
}
// only read the value of the property if really needed
NodeBuilder builder = getBuilder(e.getKey());
PropertyState p = builder.getProperty(COUNT_PROPERTY_NAME);
long count = p == null ? 0 : p.getValue(Type.LONG);
offset = ApproximateCounter.adjustOffset(count, offset, root.resolution);
if (offset == 0) {
continue;
}
updated = true;
count += offset;
if (count == 0) {
if (builder.getChildNodeCount(1) >= 0) {
builder.removeProperty(COUNT_PROPERTY_NAME);
} else {
builder.remove();
}
} else {
builder.setProperty(COUNT_PROPERTY_NAME, count);
}
}
if (updated) {
root.callback.indexUpdate();
}
}
public void leaveNew(NodeState before, NodeState after) throws CommitFailedException {
if (countOffsets.isEmpty()) {
return;
}
root.callback.indexUpdate();
for (Map.Entry<Mount, Integer> e : countOffsets.entrySet()) {
NodeBuilder builder = getBuilder(e.getKey());
int countOffset = e.getValue();
PropertyState p = builder.getProperty(COUNT_HASH_PROPERTY_NAME);
long count = p == null ? 0 : p.getValue(Type.LONG);
count += countOffset;
if (count <= 0) {
if (builder.getChildNodeCount(1) >= 0) {
builder.removeProperty(COUNT_HASH_PROPERTY_NAME);
} else {
builder.remove();
}
} else {
builder.setProperty(COUNT_HASH_PROPERTY_NAME, count);
}
}
}
private NodeBuilder getBuilder(Mount mount) {
if (parent == null) {
return root.definition.child(Multiplexers.getNodeForMount(mount, DATA_NODE_NAME));
} else {
return parent.getBuilder(mount).child(name);
}
}
private String getPath() {
if (parent == null) {
return name;
} else {
return PathUtils.concat(parent.getPath(), name);
}
}
@Override
public void propertyAdded(PropertyState after) throws CommitFailedException {
// nothing to do
}
@Override
public void propertyChanged(PropertyState before, PropertyState after)
throws CommitFailedException {
// nothing to do
}
@Override
public void propertyDeleted(PropertyState before)
throws CommitFailedException {
// nothing to do
}
@Override
@Nullable
public Editor childNodeChanged(String name, NodeState before, NodeState after)
throws CommitFailedException {
return getChildIndexEditor(name, null);
}
@Override
@Nullable
public Editor childNodeAdded(String name, NodeState after)
throws CommitFailedException {
if (NodeCounter.COUNT_HASH) {
SipHash h = new SipHash(getHash(), name.hashCode());
// with bitMask=1024: with a probability of 1:1024,
if ((h.hashCode() & root.bitMask) == 0) {
// add 1024
count(root.bitMask + 1, currentMount);
}
return getChildIndexEditor(name, h);
}
count(1, currentMount);
return getChildIndexEditor(name, null);
}
@Override
@Nullable
public Editor childNodeDeleted(String name, NodeState before)
throws CommitFailedException {
if (NodeCounter.COUNT_HASH) {
SipHash h = new SipHash(getHash(), name.hashCode());
// with bitMask=1024: with a probability of 1:1024,
if ((h.hashCode() & root.bitMask) == 0) {
// subtract 1024
count(-(root.bitMask + 1), currentMount);
}
return getChildIndexEditor(name, h);
}
count(-1, currentMount);
return getChildIndexEditor(name, null);
}
private void count(int offset, Mount mount) {
countOffsets.compute(mount, (m, v) -> v == null ? offset : v + offset);
if (parent != null) {
parent.count(offset, mount);
}
}
private Editor getChildIndexEditor(String name, SipHash hash) {
return new NodeCounterEditor(root, this, name, hash, mountInfoProvider);
}
private boolean supportMounts(String path) {
return mountInfoProvider
.getNonDefaultMounts()
.stream()
.anyMatch(m -> m.isSupportFragmentUnder(path) || m.isUnder(path));
}
public static class NodeCounterRoot {
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;
// if resolution is 1000, then the bitMask is 1023 (bits 0..9 set)
this.bitMask = (Integer.highestOneBit(resolution) * 2) - 1;
this.definition = definition;
this.root = root;
this.callback = callback;
}
}
}