/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at * trunk/opends/resource/legal-notices/OpenDS.LICENSE * or https://OpenDS.dev.java.net/OpenDS.LICENSE. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at * trunk/opends/resource/legal-notices/OpenDS.LICENSE. If applicable, * add the following below this CDDL HEADER, with the fields enclosed * by brackets "[]" replaced with your own identifying information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * * Copyright 2006-2010 Sun Microsystems, Inc. * Portions copyright 2011-2012 ForgeRock AS */ package org.opends.server.backends.jeb; import com.sleepycat.je.*; import org.opends.messages.Message; import static org.opends.server.loggers.ErrorLogger.logError; import static org.opends.server.loggers.debug.DebugLogger.*; import org.opends.server.loggers.debug.DebugTracer; import org.opends.server.api.OrderingMatchingRule; import org.opends.server.api.ApproximateMatchingRule; import org.opends.server.core.DirectoryServer; import org.opends.server.util.StaticUtils; import org.opends.server.util.ServerConstants; import org.opends.server.types.*; import static org.opends.messages.JebMessages.*; import java.util.*; import java.util.concurrent.atomic.AtomicBoolean; /** * This class is used to run an index verification process on the backend. */ public class VerifyJob { /** * The tracer object for the debug logger. */ private static final DebugTracer TRACER = getTracer(); /** * The verify configuration. */ private VerifyConfig verifyConfig; /** * The root container used for the verify job. */ RootContainer rootContainer; /** * The number of milliseconds between job progress reports. */ private long progressInterval = 10000; /** * The number of index keys processed. */ private long keyCount = 0; /** * The number of errors found. */ private long errorCount = 0; /** * The number of records that have exceeded the entry limit. */ long entryLimitExceededCount = 0; /** * The number of records that reference more than one entry. */ long multiReferenceCount = 0; /** * The total number of entry references. */ long entryReferencesCount = 0; /** * The maximum number of references per record. */ long maxEntryPerValue = 0; /** * This map is used to gather some statistics about values that have * exceeded the entry limit. */ IdentityHashMap<Index,HashMap<ByteString,Long>> entryLimitMap = new IdentityHashMap<Index, HashMap<ByteString, Long>>(); /** * Indicates whether the DN database is to be verified. */ private boolean verifyDN2ID = false; /** * Indicates whether the children database is to be verified. */ private boolean verifyID2Children = false; /** * Indicates whether the subtree database is to be verified. */ private boolean verifyID2Subtree = false; /** * The entry database. */ ID2Entry id2entry = null; /** * The DN database. */ DN2ID dn2id = null; /** * The children database. */ Index id2c = null; /** * The subtree database. */ Index id2s = null; /** * A list of the attribute indexes to be verified. */ ArrayList<AttributeIndex> attrIndexList = new ArrayList<AttributeIndex>(); /** * A list of the VLV indexes to be verified. */ ArrayList<VLVIndex> vlvIndexList = new ArrayList<VLVIndex>(); /** * The types of indexes that are verifiable. */ enum IndexType { PRES, EQ, SUBSTRING, ORDERING, APPROXIMATE } /** * Construct a VerifyJob. * * @param verifyConfig The verify configuration. */ public VerifyJob(VerifyConfig verifyConfig) { this.verifyConfig = verifyConfig; } /** * Verify the backend. * * @param rootContainer The root container that holds the entries to verify. * @param statEntry Optional statistics entry. * @return The error count. * @throws DatabaseException If an error occurs in the JE database. * @throws JebException If an error occurs in the JE backend. * @throws DirectoryException If an error occurs while verifying the backend. */ public long verifyBackend(RootContainer rootContainer, Entry statEntry) throws DatabaseException, JebException, DirectoryException { this.rootContainer = rootContainer; EntryContainer entryContainer = rootContainer.getEntryContainer(verifyConfig.getBaseDN()); entryContainer.sharedLock.lock(); try { ArrayList<String> completeList = verifyConfig.getCompleteList(); ArrayList<String> cleanList = verifyConfig.getCleanList(); boolean cleanMode = false; if (completeList.isEmpty() && cleanList.isEmpty()) { verifyDN2ID = true; if (rootContainer.getConfiguration().isSubordinateIndexesEnabled()) { verifyID2Children = true; verifyID2Subtree = true; } attrIndexList.addAll(entryContainer.getAttributeIndexes()); } else { ArrayList<String> list; if (!completeList.isEmpty()) { list = completeList; } else { list = cleanList; cleanMode = true; } for (String index : list) { String lowerName = index.toLowerCase(); if (lowerName.equals("dn2id")) { verifyDN2ID = true; } else if (lowerName.equals("id2children")) { if (rootContainer.getConfiguration().isSubordinateIndexesEnabled()) { verifyID2Children = true; } else { Message msg = NOTE_JEB_SUBORDINATE_INDEXES_DISABLED .get(rootContainer.getConfiguration().getBackendId()); throw new JebException(msg); } } else if (lowerName.equals("id2subtree")) { if (rootContainer.getConfiguration().isSubordinateIndexesEnabled()) { verifyID2Subtree = true; } else { Message msg = NOTE_JEB_SUBORDINATE_INDEXES_DISABLED .get(rootContainer.getConfiguration().getBackendId()); throw new JebException(msg); } } else if(lowerName.startsWith("vlv.")) { if(lowerName.length() < 5) { Message msg = ERR_JEB_VLV_INDEX_NOT_CONFIGURED.get(lowerName); throw new JebException(msg); } VLVIndex vlvIndex = entryContainer.getVLVIndex(lowerName.substring(4)); if(vlvIndex == null) { Message msg = ERR_JEB_VLV_INDEX_NOT_CONFIGURED.get(lowerName.substring(4)); throw new JebException(msg); } vlvIndexList.add(vlvIndex); } else { AttributeType attrType = DirectoryServer.getAttributeType(lowerName); if (attrType == null) { Message msg = ERR_JEB_ATTRIBUTE_INDEX_NOT_CONFIGURED.get(index); throw new JebException(msg); } AttributeIndex attrIndex = entryContainer.getAttributeIndex(attrType); if (attrIndex == null) { Message msg = ERR_JEB_ATTRIBUTE_INDEX_NOT_CONFIGURED.get(index); throw new JebException(msg); } attrIndexList.add(attrIndex); } } } entryLimitMap = new IdentityHashMap<Index,HashMap<ByteString,Long>>( attrIndexList.size()); // We will be updating these files independently of the indexes // so we need direct access to them rather than going through // the entry entryContainer methods. id2entry = entryContainer.getID2Entry(); dn2id = entryContainer.getDN2ID(); id2c = entryContainer.getID2Children(); id2s = entryContainer.getID2Subtree(); // Make a note of the time we started. long startTime = System.currentTimeMillis(); // Start a timer for the progress report. Timer timer = new Timer(); TimerTask progressTask = new ProgressTask(); if (cleanMode) { // Create a new progressTask based on the index count. progressTask = new ProgressTask(true); } timer.scheduleAtFixedRate(progressTask, progressInterval, progressInterval); // Iterate through the index keys. try { if (cleanMode) { iterateIndex(); } else { iterateID2Entry(); // Make sure the vlv indexes are in correct order. for(VLVIndex vlvIndex : vlvIndexList) { iterateVLVIndex(vlvIndex, false); } } } finally { timer.cancel(); } long finishTime = System.currentTimeMillis(); long totalTime = (finishTime - startTime); float rate = 0; if (totalTime > 0) { rate = 1000f*keyCount / totalTime; } addStatEntry(statEntry, "verify-error-count", String.valueOf(errorCount)); addStatEntry(statEntry, "verify-key-count", String.valueOf(keyCount)); if (cleanMode) { Message message = NOTE_JEB_VERIFY_CLEAN_FINAL_STATUS.get( keyCount, errorCount, totalTime/1000, rate); logError(message); if (multiReferenceCount > 0) { float averageEntryReferences = 0; if (keyCount > 0) { averageEntryReferences = (float)entryReferencesCount/keyCount; } message = INFO_JEB_VERIFY_MULTIPLE_REFERENCE_COUNT.get(multiReferenceCount); logError(message); addStatEntry(statEntry, "verify-multiple-reference-count", String.valueOf(multiReferenceCount)); message = INFO_JEB_VERIFY_ENTRY_LIMIT_EXCEEDED_COUNT.get( entryLimitExceededCount); logError(message); addStatEntry(statEntry, "verify-entry-limit-exceeded-count", String.valueOf(entryLimitExceededCount)); message = INFO_JEB_VERIFY_AVERAGE_REFERENCE_COUNT.get( averageEntryReferences); logError(message); addStatEntry(statEntry, "verify-average-reference-count", String.valueOf(averageEntryReferences)); message = INFO_JEB_VERIFY_MAX_REFERENCE_COUNT.get(maxEntryPerValue); logError(message); addStatEntry(statEntry, "verify-max-reference-count", String.valueOf(maxEntryPerValue)); } } else { Message message = NOTE_JEB_VERIFY_FINAL_STATUS.get( keyCount, errorCount, totalTime/1000, rate); logError(message); //TODO add entry-limit-stats to the statEntry if (entryLimitMap.size() > 0) { message = INFO_JEB_VERIFY_ENTRY_LIMIT_STATS_HEADER.get(); logError(message); for (Map.Entry<Index,HashMap<ByteString,Long>> mapEntry : entryLimitMap.entrySet()) { Index index = mapEntry.getKey(); Long[] values = mapEntry.getValue().values().toArray(new Long[0]); // Calculate the median value for entry limit exceeded. Arrays.sort(values); long medianValue; int x = values.length / 2; if (values.length % 2 == 0) { medianValue = (values[x] + values[x-1]) / 2; } else { medianValue = values[x]; } message = INFO_JEB_VERIFY_ENTRY_LIMIT_STATS_ROW. get(index.toString(), values.length, values[0], values[values.length-1], medianValue); logError(message); } } } } finally { entryContainer.sharedLock.unlock(); } return errorCount; } /** * Iterate through the entries in id2entry to perform a check for * index completeness. We check that the ID for the entry is indeed * present in the indexes for the appropriate values. * * @throws DatabaseException If an error occurs in the JE database. */ private void iterateID2Entry() throws DatabaseException { DiskOrderedCursor cursor = id2entry.openCursor(new DiskOrderedCursorConfig()); try { DatabaseEntry key = new DatabaseEntry(); DatabaseEntry data = new DatabaseEntry(); Long storedEntryCount = id2entry.getRecordCount(); while (cursor.getNext(key, data, null) == OperationStatus.SUCCESS) { EntryID entryID; try { entryID = new EntryID(key); } catch (Exception e) { errorCount++; if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); TRACER.debugError("Malformed id2entry ID %s.%n", StaticUtils.bytesToHex(key.getData())); } continue; } keyCount++; Entry entry; try { entry = ID2Entry.entryFromDatabase( ByteString.wrap(data.getData()), rootContainer.getCompressedSchema()); } catch (Exception e) { errorCount++; if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); TRACER.debugError("Malformed id2entry record for ID %d:%n%s%n", entryID.longValue(), StaticUtils.bytesToHex(data.getData())); } continue; } verifyEntry(entryID, entry); } if (keyCount != storedEntryCount) { errorCount++; if (debugEnabled()) { TRACER.debugError("The stored entry count in id2entry (%d) does " + "not agree with the actual number of entry " + "records found (%d).%n", storedEntryCount, keyCount); } } } finally { cursor.close(); } } /** * Iterate through the entries in an index to perform a check for * index cleanliness. For each ID in the index we check that the * entry it refers to does indeed contain the expected value. * * @throws JebException If an error occurs in the JE backend. * @throws DatabaseException If an error occurs in the JE database. * @throws DirectoryException If an error occurs reading values in the index. */ private void iterateIndex() throws JebException, DatabaseException, DirectoryException { if (verifyDN2ID) { iterateDN2ID(); } else if (verifyID2Children) { iterateID2Children(); } else if (verifyID2Subtree) { iterateID2Subtree(); } else { if(attrIndexList.size() > 0) { AttributeIndex attrIndex = attrIndexList.get(0); iterateAttrIndex(attrIndex.getAttributeType(), attrIndex.getEqualityIndex(), IndexType.EQ ); iterateAttrIndex(attrIndex.getAttributeType(), attrIndex.getPresenceIndex(), IndexType.PRES); iterateAttrIndex(attrIndex.getAttributeType(), attrIndex.getSubstringIndex(), IndexType.SUBSTRING); iterateAttrIndex(attrIndex.getAttributeType(), attrIndex.getOrderingIndex(), IndexType.ORDERING); iterateAttrIndex(attrIndex.getAttributeType(), attrIndex.getApproximateIndex(), IndexType.APPROXIMATE); // TODO: Need to iterate through ExtendedMatchingRules indexes. } else if(vlvIndexList.size() > 0) { iterateVLVIndex(vlvIndexList.get(0), true); } } } /** * Iterate through the entries in DN2ID to perform a check for * index cleanliness. * * @throws DatabaseException If an error occurs in the JE database. */ private void iterateDN2ID() throws DatabaseException { DiskOrderedCursor cursor = dn2id.openCursor(new DiskOrderedCursorConfig()); try { DatabaseEntry key = new DatabaseEntry(); DatabaseEntry data = new DatabaseEntry(); while (cursor.getNext(key, data, null) == OperationStatus.SUCCESS) { keyCount++; EntryID entryID; try { entryID = new EntryID(data); } catch (Exception e) { errorCount++; if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); TRACER.debugError("File dn2id has malformed ID for DN <%s>:%n%s%n", new String(key.getData()), StaticUtils.bytesToHex(data.getData())); } continue; } Entry entry; try { entry = id2entry.get(null, entryID, LockMode.DEFAULT); } catch (Exception e) { errorCount++; if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } continue; } if (entry == null) { errorCount++; if (debugEnabled()) { TRACER.debugError("File dn2id has DN <%s> referencing unknown " + "ID %d%n", new String(key.getData()), entryID.longValue()); } } else { if (!Arrays.equals(JebFormat.dnToDNKey( entry.getDN(), verifyConfig.getBaseDN().getNumComponents()), key.getData())) { errorCount++; if (debugEnabled()) { TRACER.debugError("File dn2id has DN <%s> referencing entry " + "with wrong DN <%s>%n", new String(key.getData()), entry.getDN().toNormalizedString()); } } } } } finally { cursor.close(); } } /** * Iterate through the entries in ID2Children to perform a check for * index cleanliness. * * @throws JebException If an error occurs in the JE backend. * @throws DatabaseException If an error occurs in the JE database. */ private void iterateID2Children() throws JebException, DatabaseException { DiskOrderedCursor cursor = id2c.openCursor(new DiskOrderedCursorConfig()); try { DatabaseEntry key = new DatabaseEntry(); DatabaseEntry data = new DatabaseEntry(); while (cursor.getNext(key, data, null) == OperationStatus.SUCCESS) { keyCount++; EntryID entryID; try { entryID = new EntryID(key); } catch (Exception e) { errorCount++; if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); TRACER.debugError("File id2children has malformed ID %s%n", StaticUtils.bytesToHex(key.getData())); } continue; } EntryIDSet entryIDList; try { JebFormat.entryIDListFromDatabase(data.getData()); entryIDList = new EntryIDSet(key.getData(), data.getData()); } catch (Exception e) { errorCount++; if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); TRACER.debugError("File id2children has malformed ID list " + "for ID %s:%n%s%n", entryID, StaticUtils.bytesToHex(data.getData())); } continue; } updateIndexStats(entryIDList); if (entryIDList.isDefined()) { Entry entry; try { entry = id2entry.get(null, entryID, LockMode.DEFAULT); } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } errorCount++; continue; } if (entry == null) { errorCount++; if (debugEnabled()) { TRACER.debugError("File id2children has unknown ID %d%n", entryID.longValue()); } continue; } for (EntryID id : entryIDList) { Entry childEntry; try { childEntry = id2entry.get(null, id, LockMode.DEFAULT); } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } errorCount++; continue; } if (childEntry == null) { errorCount++; if (debugEnabled()) { TRACER.debugError("File id2children has ID %d referencing " + "unknown ID %d%n", entryID.longValue(), id.longValue()); } continue; } if (!childEntry.getDN().isDescendantOf(entry.getDN()) || childEntry.getDN().getNumComponents() != entry.getDN().getNumComponents() + 1) { errorCount++; if (debugEnabled()) { TRACER.debugError("File id2children has ID %d with DN <%s> " + "referencing ID %d with non-child DN <%s>%n", entryID.longValue(), entry.getDN().toString(), id.longValue(), childEntry.getDN().toString()); } } } } } } finally { cursor.close(); } } /** * Iterate through the entries in ID2Subtree to perform a check for * index cleanliness. * * @throws JebException If an error occurs in the JE backend. * @throws DatabaseException If an error occurs in the JE database. */ private void iterateID2Subtree() throws JebException, DatabaseException { DiskOrderedCursor cursor = id2s.openCursor(new DiskOrderedCursorConfig()); try { DatabaseEntry key = new DatabaseEntry(); DatabaseEntry data = new DatabaseEntry(); while (cursor.getNext(key, data, null) == OperationStatus.SUCCESS) { keyCount++; EntryID entryID; try { entryID = new EntryID(key); } catch (Exception e) { errorCount++; if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); TRACER.debugError("File id2subtree has malformed ID %s%n", StaticUtils.bytesToHex(key.getData())); } continue; } EntryIDSet entryIDList; try { JebFormat.entryIDListFromDatabase(data.getData()); entryIDList = new EntryIDSet(key.getData(), data.getData()); } catch (Exception e) { errorCount++; if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); TRACER.debugError("File id2subtree has malformed ID list " + "for ID %s:%n%s%n", entryID, StaticUtils.bytesToHex(data.getData())); } continue; } updateIndexStats(entryIDList); if (entryIDList.isDefined()) { Entry entry; try { entry = id2entry.get(null, entryID, LockMode.DEFAULT); } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } errorCount++; continue; } if (entry == null) { errorCount++; if (debugEnabled()) { TRACER.debugError("File id2subtree has unknown ID %d%n", entryID.longValue()); } continue; } for (EntryID id : entryIDList) { Entry subordEntry; try { subordEntry = id2entry.get(null, id, LockMode.DEFAULT); } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } errorCount++; continue; } if (subordEntry == null) { errorCount++; if (debugEnabled()) { TRACER.debugError("File id2subtree has ID %d referencing " + "unknown ID %d%n", entryID.longValue(), id.longValue()); } continue; } if (!subordEntry.getDN().isDescendantOf(entry.getDN())) { errorCount++; if (debugEnabled()) { TRACER.debugError("File id2subtree has ID %d with DN <%s> " + "referencing ID %d with non-subordinate " + "DN <%s>%n", entryID.longValue(), entry.getDN().toString(), id.longValue(), subordEntry.getDN().toString()); } } } } } } finally { cursor.close(); } } /** * Increment the counter for a key that has exceeded the * entry limit. The counter gives the number of entries that have * referenced the key. * * @param index The index containing the key. * @param key A key that has exceeded the entry limit. */ private void incrEntryLimitStats(Index index, byte[] key) { HashMap<ByteString,Long> hashMap = entryLimitMap.get(index); if (hashMap == null) { hashMap = new HashMap<ByteString, Long>(); entryLimitMap.put(index, hashMap); } ByteString octetString = ByteString.wrap(key); Long counter = hashMap.get(octetString); if (counter == null) { counter = 1L; } else { counter++; } hashMap.put(octetString, counter); } /** * Update the statistical information for an index record. * * @param entryIDSet The set of entry IDs for the index record. */ private void updateIndexStats(EntryIDSet entryIDSet) { if (!entryIDSet.isDefined()) { entryLimitExceededCount++; multiReferenceCount++; } else { if (entryIDSet.size() > 1) { multiReferenceCount++; } entryReferencesCount += entryIDSet.size(); maxEntryPerValue = Math.max(maxEntryPerValue, entryIDSet.size()); } } /** * Iterate through the entries in a VLV index to perform a check for index * cleanliness. * * @param vlvIndex The VLV index to perform the check against. * @param verifyID True to verify the IDs against id2entry. * @throws JebException If an error occurs in the JE backend. * @throws DatabaseException If an error occurs in the JE database. * @throws DirectoryException If an error occurs reading values in the index. */ private void iterateVLVIndex(VLVIndex vlvIndex, boolean verifyID) throws JebException, DatabaseException, DirectoryException { if(vlvIndex == null) { return; } DiskOrderedCursor cursor = vlvIndex.openCursor(new DiskOrderedCursorConfig()); try { DatabaseEntry key = new DatabaseEntry(); DatabaseEntry data = new DatabaseEntry(); SortValues lastValues = null; while(cursor.getNext(key, data, null) == OperationStatus.SUCCESS) { SortValuesSet sortValuesSet = new SortValuesSet(key.getData(), data.getData(), vlvIndex); for(int i = 0; i < sortValuesSet.getEntryIDs().length; i++) { keyCount++; SortValues values = sortValuesSet.getSortValues(i); if(lastValues != null && lastValues.compareTo(values) >= 1) { // Make sure the values is larger then the previous one. if(debugEnabled()) { TRACER.debugError("Values %s and %s are incorrectly ordered", lastValues, values, keyDump(vlvIndex, sortValuesSet.getKeySortValues())); } errorCount++; } if(i == sortValuesSet.getEntryIDs().length - 1 && key.getData().length != 0) { // If this is the last one in a bounded set, make sure it is the // same as the database key. byte[] encodedKey = vlvIndex.encodeKey(values.getEntryID(), values.getValues()); if(!Arrays.equals(key.getData(), encodedKey)) { if(debugEnabled()) { TRACER.debugError("Incorrect key for SortValuesSet in VLV " + "index %s. Last values bytes %s, Key bytes %s", vlvIndex.getName(), encodedKey, key); } errorCount++; } } lastValues = values; if(verifyID) { Entry entry; EntryID id = new EntryID(values.getEntryID()); try { entry = id2entry.get(null, id, LockMode.DEFAULT); } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } errorCount++; continue; } if (entry == null) { errorCount++; if (debugEnabled()) { TRACER.debugError("Reference to unknown ID %d%n%s", id.longValue(), keyDump(vlvIndex, sortValuesSet.getKeySortValues())); } continue; } SortValues entryValues = new SortValues(id, entry, vlvIndex.sortOrder); if(entryValues.compareTo(values) != 0) { errorCount++; if(debugEnabled()) { TRACER.debugError("Reference to entry ID %d " + "which does not match the values%n%s", id.longValue(), keyDump(vlvIndex, sortValuesSet.getKeySortValues())); } } } } } } finally { cursor.close(); } } /** * Iterate through the entries in an attribute index to perform a check for * index cleanliness. * @param attrType The attribute type of the index to be checked. * @param index The index database to be checked. * @param indexType Type of the index (ie, SUBSTRING, ORDERING) * @throws JebException If an error occurs in the JE backend. * @throws DatabaseException If an error occurs in the JE database. */ private void iterateAttrIndex(AttributeType attrType, Index index, IndexType indexType) throws JebException, DatabaseException { if (index == null) { return; } DiskOrderedCursor cursor = index.openCursor(new DiskOrderedCursorConfig()); try { DatabaseEntry key = new DatabaseEntry(); DatabaseEntry data = new DatabaseEntry(); while (cursor.getNext(key, data, null) == OperationStatus.SUCCESS) { keyCount++; EntryIDSet entryIDList; try { JebFormat.entryIDListFromDatabase(data.getData()); entryIDList = new EntryIDSet(key.getData(), data.getData()); } catch (Exception e) { errorCount++; if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); TRACER.debugError("Malformed ID list: %s%n%s", StaticUtils.bytesToHex(data.getData()), keyDump(index, key.getData())); } continue; } updateIndexStats(entryIDList); if (entryIDList.isDefined()) { final byte[] value = key.getData(); EntryID prevID = null; for (EntryID id : entryIDList) { if (prevID != null && id.equals(prevID)) { if (debugEnabled()) { TRACER.debugError("Duplicate reference to ID %d%n%s", id.longValue(), keyDump(index, key.getData())); } } prevID = id; Entry entry; try { entry = id2entry.get(null, id, LockMode.DEFAULT); } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } errorCount++; continue; } if (entry == null) { errorCount++; if (debugEnabled()) { TRACER.debugError("Reference to unknown ID %d%n%s", id.longValue(), keyDump(index, key.getData())); } continue; } // As an optimization avoid passing in a real set and wasting time // hashing and comparing a potentially large set of values, as well // as using up memory. Instead just intercept the add() method and // detect when an equivalent value has been added. // We need to use an AtomicBoolean here since anonymous classes // require referenced external variables to be final. final AtomicBoolean foundMatchingKey = new AtomicBoolean(false); Set<byte[]> dummySet = new AbstractSet<byte[]>() { public Iterator<byte[]> iterator() { // The set is always empty. return Collections.<byte[]>emptySet().iterator(); } public int size() { // The set is always empty. return 0; } public boolean add(byte[] e) { if (Arrays.equals(e, value)) { // We could terminate processing at this point by throwing an // UnsupportedOperationException, but this optimization is // already ugly enough. foundMatchingKey.set(true); } return true; } }; index.indexer.indexEntry(entry, dummySet); if (!foundMatchingKey.get()) { errorCount++; if (debugEnabled()) { TRACER.debugError("Reference to entry " + "<%s> which does not match the value%n%s", entry.getDN(), keyDump(index, value)); } } } } } } finally { cursor.close(); } } /** * Check that an index is complete for a given entry. * * @param entryID The entry ID. * @param entry The entry to be checked. */ private void verifyEntry(EntryID entryID, Entry entry) { if (verifyDN2ID) { verifyDN2ID(entryID, entry); } if (verifyID2Children) { verifyID2Children(entryID, entry); } if (verifyID2Subtree) { verifyID2Subtree(entryID, entry); } verifyIndex(entryID, entry); } /** * Check that the DN2ID index is complete for a given entry. * * @param entryID The entry ID. * @param entry The entry to be checked. */ private void verifyDN2ID(EntryID entryID, Entry entry) { DN dn = entry.getDN(); // Check the ID is in dn2id with the correct DN. try { EntryID id = dn2id.get(null, dn, LockMode.DEFAULT); if (id == null) { if (debugEnabled()) { TRACER.debugError("File dn2id is missing key %s.%n", dn.toNormalizedString()); } errorCount++; } else if (!id.equals(entryID)) { if (debugEnabled()) { TRACER.debugError("File dn2id has ID %d instead of %d for key %s.%n", id.longValue(), entryID.longValue(), dn.toNormalizedString()); } errorCount++; } } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); TRACER.debugError("File dn2id has error reading key %s: %s.%n", dn.toNormalizedString(), e.getMessage()); } errorCount++; } // Check the parent DN is in dn2id. DN parentDN = getParent(dn); if (parentDN != null) { try { EntryID id = dn2id.get(null, parentDN, LockMode.DEFAULT); if (id == null) { if (debugEnabled()) { TRACER.debugError("File dn2id is missing key %s.%n", parentDN.toNormalizedString()); } errorCount++; } } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); TRACER.debugError("File dn2id has error reading key %s: %s.%n", parentDN.toNormalizedString(), e.getMessage()); } errorCount++; } } } /** * Check that the ID2Children index is complete for a given entry. * * @param entryID The entry ID. * @param entry The entry to be checked. */ private void verifyID2Children(EntryID entryID, Entry entry) { DN dn = entry.getDN(); DN parentDN = getParent(dn); if (parentDN != null) { EntryID parentID = null; try { parentID = dn2id.get(null, parentDN, LockMode.DEFAULT); if (parentID == null) { if (debugEnabled()) { TRACER.debugError("File dn2id is missing key %s.%n", parentDN.toNormalizedString()); } errorCount++; } } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); TRACER.debugError("File dn2id has error reading key %s: %s.", parentDN.toNormalizedString(), e.getMessage()); } errorCount++; } if (parentID != null) { try { ConditionResult cr; cr = id2c.containsID(null, parentID.getDatabaseEntry(), entryID); if (cr == ConditionResult.FALSE) { if (debugEnabled()) { TRACER.debugError("File id2children is missing ID %d " + "for key %d.%n", entryID.longValue(), parentID.longValue()); } errorCount++; } else if (cr == ConditionResult.UNDEFINED) { incrEntryLimitStats(id2c, parentID.getDatabaseEntry().getData()); } } catch (DatabaseException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); TRACER.debugError("File id2children has error reading key %d: %s.", parentID.longValue(), e.getMessage()); } errorCount++; } } } } /** * Check that the ID2Subtree index is complete for a given entry. * * @param entryID The entry ID. * @param entry The entry to be checked. */ private void verifyID2Subtree(EntryID entryID, Entry entry) { for (DN dn = getParent(entry.getDN()); dn != null; dn = getParent(dn)) { EntryID id = null; try { id = dn2id.get(null, dn, LockMode.DEFAULT); if (id == null) { if (debugEnabled()) { TRACER.debugError("File dn2id is missing key %s.%n", dn.toNormalizedString()); } errorCount++; } } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); TRACER.debugError("File dn2id has error reading key %s: %s.%n", dn.toNormalizedString(), e.getMessage()); } errorCount++; } if (id != null) { try { ConditionResult cr; cr = id2s.containsID(null, id.getDatabaseEntry(), entryID); if (cr == ConditionResult.FALSE) { if (debugEnabled()) { TRACER.debugError("File id2subtree is missing ID %d " + "for key %d.%n", entryID.longValue(), id.longValue()); } errorCount++; } else if (cr == ConditionResult.UNDEFINED) { incrEntryLimitStats(id2s, id.getDatabaseEntry().getData()); } } catch (DatabaseException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); TRACER.debugError("File id2subtree has error reading key %d: %s.%n", id.longValue(), e.getMessage()); } errorCount++; } } } } /** * Construct a printable string from a raw key value. * * @param index The index database containing the key value. * @param keyBytes The bytes of the key. * @return A string that may be logged or printed. */ private String keyDump(Index index, byte[] keyBytes) { /* String str; try { str = new String(keyBytes, "UTF-8"); } catch (UnsupportedEncodingException e) { str = StaticUtils.bytesToHex(keyBytes); } return str; */ StringBuilder buffer = new StringBuilder(128); buffer.append("File: "); buffer.append(index.toString()); buffer.append(ServerConstants.EOL); buffer.append("Key:"); buffer.append(ServerConstants.EOL); StaticUtils.byteArrayToHexPlusAscii(buffer, keyBytes, 6); return buffer.toString(); } /** * Construct a printable string from a raw key value. * * @param vlvIndex The vlvIndex database containing the key value. * @param keySortValues THe sort values that is being used as the key. * @return A string that may be logged or printed. */ private String keyDump(VLVIndex vlvIndex, SortValues keySortValues) { /* String str; try { str = new String(keyBytes, "UTF-8"); } catch (UnsupportedEncodingException e) { str = StaticUtils.bytesToHex(keyBytes); } return str; */ StringBuilder buffer = new StringBuilder(128); buffer.append("File: "); buffer.append(vlvIndex.toString()); buffer.append(ServerConstants.EOL); buffer.append("Key (last sort values):"); if(keySortValues == null) { buffer.append("UNBOUNDED (0x00)"); } else { buffer.append(keySortValues.toString()); } return buffer.toString(); } /** * Check that an attribute index is complete for a given entry. * * @param entryID The entry ID. * @param entry The entry to be checked. */ private void verifyIndex(EntryID entryID, Entry entry) { for (AttributeIndex attrIndex : attrIndexList) { try { List<Attribute> attrList = entry.getAttribute(attrIndex.getAttributeType()); if (attrList != null) { verifyAttribute(attrIndex, entryID, attrList); } } catch (DirectoryException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); TRACER.debugError("Error normalizing values of attribute %s in " + "entry <%s>: %s.%n", attrIndex.getAttributeType().toString(), entry.getDN().toString(), String.valueOf(e.getMessageObject())); } } } for (VLVIndex vlvIndex : vlvIndexList) { try { if(vlvIndex.shouldInclude(entry)) { if(!vlvIndex.containsValues(null, entryID.longValue(), vlvIndex.getSortValues(entry))) { if(debugEnabled()) { TRACER.debugError("Missing entry %s in VLV index %s", entry.getDN().toString(), vlvIndex.getName()); } errorCount++; } } } catch (DirectoryException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); TRACER.debugError("Error checking entry %s against filter or " + "base DN for VLV index %s: %s", entry.getDN().toString(), vlvIndex.getName(), String.valueOf(e.getMessageObject())); } errorCount++; } catch (DatabaseException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); TRACER.debugError("Error reading VLV index %s for entry %s: %s", vlvIndex.getName(), entry.getDN().toString(), StaticUtils.getBacktrace(e)); } errorCount++; } catch (JebException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); TRACER.debugError("Error reading VLV index %s for entry %s: %s", vlvIndex.getName(), entry.getDN().toString(), StaticUtils.getBacktrace(e)); } errorCount++; } } } /** * Check that an attribute index is complete for a given attribute. * * @param attrIndex The attribute index to be checked. * @param entryID The entry ID. * @param attrList The attribute to be checked. * @throws DirectoryException If a Directory Server error occurs. */ private void verifyAttribute(AttributeIndex attrIndex, EntryID entryID, List<Attribute> attrList) throws DirectoryException { Transaction txn = null; Index equalityIndex = attrIndex.getEqualityIndex(); Index presenceIndex = attrIndex.getPresenceIndex(); Index substringIndex = attrIndex.getSubstringIndex(); Index orderingIndex = attrIndex.getOrderingIndex(); Index approximateIndex = attrIndex.getApproximateIndex(); // TODO: Add support for Extended Matching Rules indexes. DatabaseEntry presenceKey = AttributeIndex.presenceKey; // Presence index. if ((attrList != null) && !attrList.isEmpty() && presenceIndex != null) { try { ConditionResult cr; cr = presenceIndex.containsID(txn, presenceKey, entryID); if (cr == ConditionResult.FALSE) { if (debugEnabled()) { TRACER.debugError("Missing ID %d%n%s", entryID.longValue(), keyDump(presenceIndex, presenceKey.getData())); } errorCount++; } else if (cr == ConditionResult.UNDEFINED) { incrEntryLimitStats(presenceIndex, presenceKey.getData()); } } catch (DatabaseException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); TRACER.debugError("Error reading database: %s%n%s", e.getMessage(), keyDump(presenceIndex, presenceKey.getData())); } errorCount++; } } if (attrList != null) { for (Attribute attr : attrList) { for (AttributeValue value : attr) { byte[] normalizedBytes = value.getNormalizedValue().toByteArray(); // Equality index. if (equalityIndex != null) { DatabaseEntry key = new DatabaseEntry(normalizedBytes); try { ConditionResult cr; cr = equalityIndex.containsID(txn, key, entryID); if (cr == ConditionResult.FALSE) { if (debugEnabled()) { TRACER.debugError("Missing ID %d%n%s", entryID.longValue(), keyDump(equalityIndex, normalizedBytes)); } errorCount++; } else if (cr == ConditionResult.UNDEFINED) { incrEntryLimitStats(equalityIndex, normalizedBytes); } } catch (DatabaseException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); TRACER.debugError("Error reading database: %s%n%s", e.getMessage(), keyDump(equalityIndex, normalizedBytes)); } errorCount++; } } // Substring index. if (substringIndex != null) { Set<ByteString> keyBytesSet = attrIndex.substringKeys(normalizedBytes); DatabaseEntry key = new DatabaseEntry(); for (ByteString keyBytes : keyBytesSet) { key.setData(keyBytes.toByteArray()); try { ConditionResult cr; cr = substringIndex.containsID(txn, key, entryID); if (cr == ConditionResult.FALSE) { if (debugEnabled()) { TRACER.debugError("Missing ID %d%n%s", entryID.longValue(), keyDump(substringIndex, key.getData())); } errorCount++; } else if (cr == ConditionResult.UNDEFINED) { incrEntryLimitStats(substringIndex, key.getData()); } } catch (DatabaseException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); TRACER.debugError("Error reading database: %s%n%s", e.getMessage(), keyDump(substringIndex, key.getData())); } errorCount++; } } } // Ordering index. if (orderingIndex != null) { // Use the ordering matching rule to normalize the value. OrderingMatchingRule orderingRule = attr.getAttributeType().getOrderingMatchingRule(); normalizedBytes = orderingRule.normalizeValue(value.getValue()).toByteArray(); DatabaseEntry key = new DatabaseEntry(normalizedBytes); try { ConditionResult cr; cr = orderingIndex.containsID(txn, key, entryID); if (cr == ConditionResult.FALSE) { if (debugEnabled()) { TRACER.debugError("Missing ID %d%n%s", entryID.longValue(), keyDump(orderingIndex, normalizedBytes)); } errorCount++; } else if (cr == ConditionResult.UNDEFINED) { incrEntryLimitStats(orderingIndex, normalizedBytes); } } catch (DatabaseException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); TRACER.debugError("Error reading database: %s%n%s", e.getMessage(), keyDump(orderingIndex, normalizedBytes)); } errorCount++; } } // Approximate index. if (approximateIndex != null) { // Use the approximate matching rule to normalize the value. ApproximateMatchingRule approximateRule = attr.getAttributeType().getApproximateMatchingRule(); normalizedBytes = approximateRule.normalizeValue(value.getValue()).toByteArray(); DatabaseEntry key = new DatabaseEntry(normalizedBytes); try { ConditionResult cr; cr = approximateIndex.containsID(txn, key, entryID); if (cr == ConditionResult.FALSE) { if (debugEnabled()) { TRACER.debugError("Missing ID %d%n%s", entryID.longValue(), keyDump(orderingIndex, normalizedBytes)); } errorCount++; } else if (cr == ConditionResult.UNDEFINED) { incrEntryLimitStats(orderingIndex, normalizedBytes); } } catch (DatabaseException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); TRACER.debugError("Error reading database: %s%n%s", e.getMessage(), keyDump(approximateIndex, normalizedBytes)); } errorCount++; } } } } } } /** * Get the parent DN of a given DN. * * @param dn The DN. * @return The parent DN or null if the given DN is a base DN. */ private DN getParent(DN dn) { if (dn.equals(verifyConfig.getBaseDN())) { return null; } return dn.getParentDNInSuffix(); } /** * This class reports progress of the verify job at fixed intervals. */ class ProgressTask extends TimerTask { /** * The total number of records to process. */ private long totalCount; /** * The number of records that had been processed at the time of the * previous progress report. */ private long previousCount = 0; /** * The time in milliseconds of the previous progress report. */ private long previousTime; /** * The environment statistics at the time of the previous report. */ private EnvironmentStats prevEnvStats; /** * The number of bytes in a megabyte. * Note that 1024*1024 bytes may eventually become known as a mebibyte(MiB). */ private static final int bytesPerMegabyte = 1024*1024; /** * Create a new verify progress task. * @throws DatabaseException An error occurred while accessing the JE * database. */ public ProgressTask() throws DatabaseException { previousTime = System.currentTimeMillis(); prevEnvStats = rootContainer.getEnvironmentStats(new StatsConfig()); totalCount = rootContainer.getEntryContainer( verifyConfig.getBaseDN()).getEntryCount(); } /** * Create a new verify progress task. * @param indexIterator boolean, indicates if the task is iterating * through indexes or the entries. * @throws DatabaseException An error occurred while accessing the JE * database. */ public ProgressTask(boolean indexIterator) throws DatabaseException { previousTime = System.currentTimeMillis(); prevEnvStats = rootContainer.getEnvironmentStats(new StatsConfig()); if (indexIterator) { if (verifyDN2ID) { totalCount = dn2id.getRecordCount(); } else if (verifyID2Children) { totalCount = id2c.getRecordCount(); } else if (verifyID2Subtree) { totalCount = id2s.getRecordCount(); } else { if(attrIndexList.size() > 0) { AttributeIndex attrIndex = attrIndexList.get(0); totalCount = 0; if (attrIndex.getEqualityIndex() != null) { totalCount += attrIndex.getEqualityIndex().getRecordCount(); } if (attrIndex.getPresenceIndex() != null) { totalCount += attrIndex.getPresenceIndex().getRecordCount(); } if (attrIndex.getSubstringIndex() != null) { totalCount += attrIndex.getSubstringIndex().getRecordCount(); } if (attrIndex.getOrderingIndex() != null) { totalCount += attrIndex.getOrderingIndex().getRecordCount(); } if (attrIndex.getApproximateIndex() != null) { totalCount += attrIndex.getApproximateIndex().getRecordCount(); } // TODO: Add support for Extended Matching Rules indexes. } else if(vlvIndexList.size() > 0) { totalCount = vlvIndexList.get(0).getRecordCount(); } } } else { totalCount = rootContainer.getEntryContainer( verifyConfig.getBaseDN()).getEntryCount(); } } /** * The action to be performed by this timer task. */ public void run() { long latestCount = keyCount; long deltaCount = (latestCount - previousCount); long latestTime = System.currentTimeMillis(); long deltaTime = latestTime - previousTime; if (deltaTime == 0) { return; } float rate = 1000f*deltaCount / deltaTime; Message message = NOTE_JEB_VERIFY_PROGRESS_REPORT.get( latestCount, totalCount, errorCount, rate); logError(message); try { Runtime runtime = Runtime.getRuntime(); long freeMemory = runtime.freeMemory() / bytesPerMegabyte; EnvironmentStats envStats = rootContainer.getEnvironmentStats(new StatsConfig()); long nCacheMiss = envStats.getNCacheMiss() - prevEnvStats.getNCacheMiss(); float cacheMissRate = 0; if (deltaCount > 0) { cacheMissRate = nCacheMiss/(float)deltaCount; } message = INFO_JEB_VERIFY_CACHE_AND_MEMORY_REPORT.get( freeMemory, cacheMissRate); logError(message); prevEnvStats = envStats; } catch (DatabaseException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } } previousCount = latestCount; previousTime = latestTime; } } /** * Adds an attribute of type t and value v to the statEntry, only if the * statEntry is not null. * @param statEntry passed in from backentryImpl.verifyBackend. * @param t String to be used as the attribute type. * @param v String to be used as the attribute value. */ private void addStatEntry(Entry statEntry, String t, String v) { if (statEntry != null) { Attribute a = Attributes.create(t, v); statEntry.addAttribute(a, null); } } }