/* * The contents of this file are subject to the terms of the Common Development and * Distribution License (the License). You may not use this file except in compliance with the * License. * * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the * specific language governing permission and limitations under the License. * * When distributing Covered Software, include this CDDL Header Notice in each file and include * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions Copyright [year] [name of copyright owner]". * * Copyright 2006-2010 Sun Microsystems, Inc. * Portions Copyright 2014-2016 ForgeRock AS. */ package org.opends.server.backends.pluggable; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import org.forgerock.i18n.LocalizableMessage; import org.forgerock.opendj.server.config.server.MonitorProviderCfg; import org.opends.server.api.MonitorData; import org.opends.server.api.MonitorProvider; import org.opends.server.types.SearchFilter; import org.opends.server.util.TimeThread; /** * A monitor provider for high level backend statistics, such as filter stats and search counters. */ class BackendMonitor extends MonitorProvider { /** Represents the statistical information kept for each search filter. */ private static final class FilterStats implements Comparable { private volatile LocalizableMessage failureReason = LocalizableMessage.EMPTY; private long maxMatchingEntries = -1; private final AtomicInteger hits = new AtomicInteger(); @Override public int compareTo(FilterStats that) { return this.hits.get() - that.hits.get(); } private void update(int hitCount, LocalizableMessage failureReason) { this.hits.getAndAdd(hitCount); this.failureReason = failureReason; } private void update(int hitCount, long matchingEntries) { this.hits.getAndAdd(hitCount); this.failureReason = LocalizableMessage.EMPTY; synchronized (this) { if (matchingEntries > maxMatchingEntries) { maxMatchingEntries = matchingEntries; } } } } /** The name of this monitor instance. */ private final String name; /** The root container to be monitored. */ private final RootContainer rootContainer; private int maxEntries = 1024; private boolean filterUseEnabled; private String startTimeStamp; private final HashMap filterToStats = new HashMap<>(); private final AtomicInteger indexedSearchCount = new AtomicInteger(); private final AtomicInteger unindexedSearchCount = new AtomicInteger(); /** * Creates a new backend monitor. * @param name The monitor instance name. * @param rootContainer A root container handle for the backend to be * monitored. */ BackendMonitor(String name, RootContainer rootContainer) { this.name = name; this.rootContainer = rootContainer; } @Override public String getMonitorInstanceName() { return name; } @Override public MonitorData getMonitorData() { MonitorData monitorAttrs = new MonitorData(5); Collection needReindexValues = createNeedReindexValues(); if (!needReindexValues.isEmpty()) { monitorAttrs.add("need-reindex", needReindexValues); } if (filterUseEnabled) { monitorAttrs.add("filter-use-startTime", startTimeStamp); monitorAttrs.add("filter-use", createFilterUseValues()); monitorAttrs.add("filter-use-indexed", indexedSearchCount); monitorAttrs.add("filter-use-unindexed", unindexedSearchCount); } return monitorAttrs; } private Collection createNeedReindexValues() { Collection values = new ArrayList<>(); for (EntryContainer ec : rootContainer.getEntryContainers()) { for (Tree tree : ec.listTrees()) { if (tree instanceof Index && !((Index) tree).isTrusted()) { values.add(tree.getName().toString()); } } } return values; } private List createFilterUseValues() { List values = new ArrayList<>(); StringBuilder value = new StringBuilder(); synchronized (filterToStats) { for (Map.Entry entry : filterToStats.entrySet()) { entry.getKey().toString(value); value.append(" hits:").append(entry.getValue().hits.get()); value.append(" maxmatches:").append(entry.getValue().maxMatchingEntries); value.append(" message:").append(entry.getValue().failureReason); values.add(value.toString()); value.setLength(0); } } return values; } /** * Updates the index filter statistics with this latest search filter * and the reason why an index was not used. * * @param searchFilter The search filter that was evaluated. * @param failureMessage The reason why an index was not used. */ void updateStats(SearchFilter searchFilter, LocalizableMessage failureMessage) { if (!filterUseEnabled) { return; } FilterStats stats; synchronized (filterToStats) { stats = filterToStats.get(searchFilter); if (stats != null) { stats.update(1, failureMessage); } else { stats = new FilterStats(); stats.update(1, failureMessage); removeLowestHit(); filterToStats.put(searchFilter, stats); } } } /** * Updates the index filter statistics with this latest search filter * and the number of entries matched by the index lookup. * * @param searchFilter The search filter that was evaluated. * @param matchingEntries The number of entries matched by the successful * index lookup. */ void updateStats(SearchFilter searchFilter, long matchingEntries) { if (!filterUseEnabled) { return; } FilterStats stats; synchronized (filterToStats) { stats = filterToStats.get(searchFilter); if (stats != null) { stats.update(1, matchingEntries); } else { stats = new FilterStats(); stats.update(1, matchingEntries); removeLowestHit(); filterToStats.put(searchFilter, stats); } } } /** * Enable or disable index filter statistics gathering. * * @param enabled true> to enable index filter statics gathering. */ void enableFilterUseStats(boolean enabled) { if (enabled && !filterUseEnabled) { startTimeStamp = TimeThread.getGMTTime(); indexedSearchCount.set(0); unindexedSearchCount.set(0); } else if (!enabled) { filterToStats.clear(); } filterUseEnabled = enabled; } /** * Indicates if index filter statistics gathering is enabled. * * @return true If index filter statistics gathering is enabled. */ boolean isFilterUseEnabled() { return filterUseEnabled; } /** * Sets the maximum number of search filters statistics entries to keep * before ones with the least hits will be removed. * * @param maxEntries The maximum number of search filters statistics * entries to keep */ void setMaxEntries(int maxEntries) { this.maxEntries = maxEntries; } /** Increments the statistics counter to include an indexed search. */ void incrementIndexedSearchCount() { if (filterUseEnabled) { indexedSearchCount.getAndIncrement(); } } /** Increments the statistics counter to include an unindexed search. */ void incrementUnindexedSearchCount() { if (filterUseEnabled) { unindexedSearchCount.getAndIncrement(); } } private void removeLowestHit() { while (!filterToStats.isEmpty() && filterToStats.size() > maxEntries) { Iterator> it = filterToStats.entrySet().iterator(); Map.Entry lowest = it.next(); Map.Entry entry; while (lowest.getValue().hits.get() > 1 && it.hasNext()) { entry = it.next(); if (entry.getValue().hits.get() < lowest.getValue().hits.get()) { lowest = entry; } } filterToStats.remove(lowest.getKey()); } } }