| /* |
| * 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); |
| } |
| |
| } |