blob: fa11e76f271d541a08b57d32bb9b858a24a13c30 [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.jmx;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Objects;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
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.commons.jmx.AnnotatedStandardMBean;
import org.apache.jackrabbit.oak.plugins.index.IndexConstants;
import org.apache.jackrabbit.oak.plugins.index.counter.NodeCounterEditor;
import org.apache.jackrabbit.oak.plugins.index.property.Multiplexers;
import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry;
import org.apache.jackrabbit.oak.spi.state.NodeState;
import org.apache.jackrabbit.oak.spi.state.NodeStore;
import org.apache.jackrabbit.oak.plugins.index.counter.ApproximateCounter;
/**
* A mechanism to retrieve node counter data.
*/
public class NodeCounter extends AnnotatedStandardMBean implements NodeCounterMBean {
/**
* Approximate count using the hashed name (deterministically, so that after
* adding a removing all nodes the count goes back to zero).
*/
public final static boolean COUNT_HASH =
Boolean.parseBoolean(System.getProperty("oak.countHashed", "true"));
public static final boolean USE_OLD_COUNTER = Boolean.getBoolean("oak.index.useCounterOld");
private final NodeStore store;
public NodeCounter(NodeStore store) {
super(NodeCounterMBean.class);
this.store = store;
}
private static NodeState child(NodeState n, String... path) {
return child(n, Arrays.asList(path));
}
private static NodeState child(NodeState n, Iterable<String> path) {
for (String p : path) {
if (n == null) {
break;
}
if (p.length() > 0) {
n = n.getChildNode(p);
}
}
return n;
}
@Override
public long getEstimatedNodeCount(String path) {
return getEstimatedNodeCount(store.getRoot(), path, false);
}
/**
* Get the estimated number of nodes for a given path.
*
* @param root the root
* @param path the path
* @param max whether to get the maximum expected number of nodes (the
* stored value plus the resolution)
* @return -1 if unknown, 0 if the node does not exist (or, if max is false,
* if there are probably not many descendant nodes), or the
* (maximum) estimated number of descendant nodes
*/
public static long getEstimatedNodeCount(NodeState root, String path, boolean max) {
if (USE_OLD_COUNTER) {
return NodeCounterOld.getEstimatedNodeCount(root, path, max);
} else {
return doGetEstimatedNodeCount(root, path, max);
}
}
private static long doGetEstimatedNodeCount(NodeState root, String path, boolean max) {
// check if there is a property in the node itself
// (for property index nodes)
NodeState s = child(root,
PathUtils.elements(path));
if (s == null || !s.exists()) {
// node not found
return 0;
}
if (!max) {
long syncCount = ApproximateCounter.getCountSync(s);
if (syncCount != -1) {
return syncCount;
}
}
if (COUNT_HASH) {
return getCombinedCount(root, path, s, max);
}
return getEstimatedNodeCountOld(root, s, path, max);
}
private static long getEstimatedNodeCountOld(NodeState root, NodeState s, String path, boolean max) {
// old code from here
PropertyState p = s.getProperty(NodeCounterEditor.COUNT_PROPERTY_NAME);
if (p != null) {
long x = p.getValue(Type.LONG);
if (max) {
// in the node itself, we just add the resolution
x += ApproximateCounter.COUNT_RESOLUTION;
}
return x;
}
// check in the counter index (if it exists)
s = child(root,
IndexConstants.INDEX_DEFINITIONS_NAME,
"counter");
if (s == null || !s.exists()) {
// no index
return -1;
}
if (!dataNodeExists(s)) {
// no index data (not yet indexed, or very few nodes)
return -1;
}
long sum = getIndexingData(s, path)
.map(n -> n.getProperty(NodeCounterEditor.COUNT_PROPERTY_NAME))
.filter(Objects::nonNull)
.mapToLong(v -> v.getValue(Type.LONG))
.sum();
if (sum == 0) {
return max ? ApproximateCounter.COUNT_RESOLUTION * 20 : 0;
} else {
return sum + (max ? ApproximateCounter.COUNT_RESOLUTION : 0);
}
}
private static long getCombinedCount(NodeState root, String path, NodeState s, boolean max) {
Long value = getCombinedCountIfAvailable(s);
if (value != null) {
return value + (max ? ApproximateCounter.COUNT_RESOLUTION : 0);
}
// check in the counter index (if it exists)
s = child(root,
IndexConstants.INDEX_DEFINITIONS_NAME,
"counter");
if (s == null || !s.exists()) {
// no index
return -1;
}
if (!dataNodeExists(s)) {
// no index data (not yet indexed, or very few nodes)
return -1;
}
long sum = getIndexingData(s, path)
.map(NodeCounter::getCombinedCountIfAvailable)
.filter(Objects::nonNull)
.mapToLong(Long::longValue)
.sum();
if (sum == 0) {
return max ? ApproximateCounter.COUNT_RESOLUTION * 20 : 0;
} else {
return sum + (max ? ApproximateCounter.COUNT_RESOLUTION : 0);
}
}
private static Long getCombinedCountIfAvailable(NodeState s) {
boolean found = false;
long x = 0;
PropertyState p = s.getProperty(NodeCounterEditor.COUNT_HASH_PROPERTY_NAME);
if (p != null) {
found = true;
x = p.getValue(Type.LONG);
}
p = s.getProperty(NodeCounterEditor.COUNT_PROPERTY_NAME);
if (p != null) {
found = true;
x += p.getValue(Type.LONG);
}
return found ? x : null;
}
@Override
public String getEstimatedChildNodeCounts(String path, int level) {
StringBuilder buff = new StringBuilder();
collectCounts(buff, path, level);
return buff.toString();
}
private void collectCounts(StringBuilder buff, String path, int level) {
long count = getEstimatedNodeCount(path);
if (count > 0) {
if (buff.length() > 0) {
buff.append(",\n");
}
buff.append(path).append(": ").append(count);
}
NodeState s = child(store.getRoot(),
PathUtils.elements(path));
if (!s.exists()) {
return;
}
if (count == 0 && level < 0) {
if (buff.length() > 0) {
buff.append(",\n");
}
buff.append(path).append(": 0");
}
if (level <= 0) {
return;
}
ArrayList<String> names = new ArrayList<String>();
for (ChildNodeEntry c : s.getChildNodeEntries()) {
names.add(c.getName());
}
Collections.sort(names);
for (String cn : names) {
s.getChildNode(cn);
String child = PathUtils.concat(path, cn);
collectCounts(buff, child, level - 1);
}
}
private static Stream<NodeState> getIndexingData(NodeState indexDefinition, String path) {
Iterable<String> pathElements = PathUtils.elements(path);
return StreamSupport.stream(indexDefinition.getChildNodeEntries().spliterator(), false)
.filter(NodeCounter::isDataNodeName)
.map(ChildNodeEntry::getNodeState)
.map(n -> child(n, pathElements))
.filter(Objects::nonNull)
.filter(NodeState::exists);
}
private static boolean isDataNodeName(ChildNodeEntry childNodeEntry) {
String name = childNodeEntry.getName();
return NodeCounterEditor.DATA_NODE_NAME.equals(name)
|| (name.startsWith(":") && name.endsWith("-" + Multiplexers.stripStartingColon(NodeCounterEditor.DATA_NODE_NAME)));
}
private static boolean dataNodeExists(NodeState indexDefinition) {
return StreamSupport
.stream(indexDefinition.getChildNodeEntries().spliterator(), false)
.anyMatch(NodeCounter::isDataNodeName);
}
}