com.sleepycat.je.cleaner.FileProcessor Maven / Gradle / Ivy
/*-
* Copyright (C) 2002, 2018, Oracle and/or its affiliates. All rights reserved.
*
* This file was distributed by Oracle as part of a version of Oracle Berkeley
* DB Java Edition made available at:
*
* http://www.oracle.com/technetwork/database/database-technologies/berkeleydb/downloads/index.html
*
* Please see the LICENSE file included in the top-level directory of the
* appropriate version of Oracle Berkeley DB Java Edition for a copy of the
* license and additional information.
*/
package com.sleepycat.je.cleaner;
import static com.sleepycat.je.ExtinctionFilter.ExtinctionStatus.EXTINCT;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.logging.Level;
import com.sleepycat.je.CacheMode;
import com.sleepycat.je.DiskLimitException;
import com.sleepycat.je.EnvironmentFailureException;
import com.sleepycat.je.cleaner.DbCache.DbInfo;
import com.sleepycat.je.dbi.CursorImpl;
import com.sleepycat.je.dbi.DatabaseId;
import com.sleepycat.je.dbi.DatabaseImpl;
import com.sleepycat.je.dbi.DbTree;
import com.sleepycat.je.dbi.EnvironmentFailureReason;
import com.sleepycat.je.dbi.EnvironmentImpl;
import com.sleepycat.je.dbi.MemoryBudget;
import com.sleepycat.je.dbi.TTL;
import com.sleepycat.je.log.ChecksumException;
import com.sleepycat.je.log.CleanerFileReader;
import com.sleepycat.je.log.LogItem;
import com.sleepycat.je.log.Trace;
import com.sleepycat.je.log.entry.LNLogEntry;
import com.sleepycat.je.tree.BIN;
import com.sleepycat.je.tree.ChildReference;
import com.sleepycat.je.tree.IN;
import com.sleepycat.je.tree.LN;
import com.sleepycat.je.tree.MapLN;
import com.sleepycat.je.tree.OldBINDelta;
import com.sleepycat.je.tree.SearchResult;
import com.sleepycat.je.tree.Tree;
import com.sleepycat.je.tree.TreeLocation;
import com.sleepycat.je.tree.WithRootLatched;
import com.sleepycat.je.txn.BasicLocker;
import com.sleepycat.je.txn.LockGrantType;
import com.sleepycat.je.txn.LockManager;
import com.sleepycat.je.txn.LockResult;
import com.sleepycat.je.txn.LockType;
import com.sleepycat.je.utilint.DaemonThread;
import com.sleepycat.je.utilint.DbLsn;
import com.sleepycat.je.utilint.LoggerUtils;
import com.sleepycat.je.utilint.Pair;
import com.sleepycat.je.utilint.TestHookExecute;
/**
* Reads all entries in a log file and either determines them to be obsolete or
* active. Active LNs are migrated immediately (by logging them). Active INs
* are marked for migration by setting the dirty flag.
*
* May be invoked explicitly by calling doClean, or woken up if used as a
* daemon thread.
*/
public class FileProcessor extends DaemonThread {
/**
* The number of LN log entries after we process pending LNs. If we do
* this too seldom, the pending LN queue may grow large, and it isn't
* budgeted memory. If we process it too often, we will repeatedly request
* a non-blocking lock for the same locked node.
*/
private static final int PROCESS_PENDING_EVERY_N_LNS = 100;
private final Cleaner cleaner;
private final FileSelector fileSelector;
private final UtilizationProfile profile;
private final UtilizationCalculator calculator;
/** @see #onWakeup() */
private volatile boolean activate = false;
private long lastWakeupLsn = 0;
/*
* The first thread (out of N cleaner threads) does certain housekeeping
* duties that don't need to be done by all threads.
*/
private final boolean firstThread;
/* Log version for the target file. */
private int fileLogVersion;
/* Per Run counters. Reset before each file is processed. */
/*
* Number of full IN (BIN or UIN) logrecs that were known to be apriory
* obsolete and did not need any further processing (i.e., they did not
* need to be searched-for in the tree). These include logrecs whose
* offset is recorded in the FS DB, or whose DB has been deleted or is
* being deleted.
*/
private int nINsObsoleteThisRun = 0;
/*
* Number of full IN (BIN or UIN) logrecs that were not known apriory to
* be obsolete, and as a result, needed further processing.
*/
private int nINsCleanedThisRun = 0;
/*
* Number of full IN (BIN or UIN) logrecs that were found to be obsolete
* after having been looked up in the tree.
*/
private int nINsDeadThisRun = 0;
/*
* Number of full IN (BIN or UIN) logrecs that were still active and were
* marked dirty so that they will be migrated during the next ckpt.
*/
private int nINsMigratedThisRun = 0;
/*
* Number of BIN-delta logrecs that were known to be apriory
* obsolete and did not need any further processing (i.e., they did not
* need to be searched-for in the tree). These include logrecs whose
* offset is recorded in the FS DB, or whose DB has been deleted or is
* being deleted.
*/
private int nBINDeltasObsoleteThisRun = 0;
/*
* Number of BIN-delta logrecs that were not known apriory to be obsolete,
* and as a result, needed further processing.
*/
private int nBINDeltasCleanedThisRun = 0;
/*
* Number of BIN-delta logrecs that were found to be obsolete after having
* been looked up in the tree.
*/
private int nBINDeltasDeadThisRun = 0;
/*
* Number of BIN-delta logrecs that were still active and were marked
* dirty so that they will be migrated during the next ckpt.
*/
private int nBINDeltasMigratedThisRun = 0;
/*
* Number of LN logrecs that were known to be apriory obsolete and did not
* need any further processing (for example, they did not need to be
* searched-for in the tree). These include logrecs that are immediately
* obsolete, or whose offset is recorded in the FS DB, or whose DB has been
* deleted or is being deleted.
*/
private int nLNsObsoleteThisRun = 0;
/*
* Number of LN logrecs that were expired.
*/
private int nLNsExpiredThisRun = 0;
/*
* Number of LN logrecs that were extinct.
*/
private int nLNsExtinctThisRun = 0;
/*
* Number of LN logrecs that were not known apriory to be obsolete, and as
* a result, needed further processing. These include LNs that had to be
* searched-for in the tree as well as the nLNQueueHitsThisRun (see below).
*/
private int nLNsCleanedThisRun = 0;
/*
* Number of LN logrecs that were processed without tree search. Let L1 and
* L2 be two LN logrecs and R1 and R2 be their associated records. We will
* avoid a tree search for L1 if L1 is in the to-be-proccessed cache when
* L2 is processed, R2 must be searched-for in the tree, R2 is found in a
* BIN B, and L1 is also pointed-to by a slot in B.
*/
private int nLNQueueHitsThisRun = 0;
/*
* Number of LN logrecs that were found to be obsolete after haning been
* processed further.
*/
private int nLNsDeadThisRun = 0;
/*
* Number of LN logrecs whose LSN had to be locked in order to check their
* obsolescence, and this non-blocking lock request was denied (and as a
* result, the logrec was placed in the "pending LNs" queue.
*/
private int nLNsLockedThisRun = 0;
/*
* Number of LN logrecs that were still active and were migrated.
*/
private int nLNsMigratedThisRun = 0;
/*
* This applies to temporary DBs only. It is the number of LN logrecs that
* were still active, but instead of migrating them, we attached the LN to
* the memory-resident tree and marked the LN as dirty.
*/
private int nLNsMarkedThisRun = 0;
/*
* Number of DbTree.getDb lookups during cleaning.
*/
private int nDbLookupsThisRun = 0;
/*
* Number of log entries read during cleaning.
*/
private int nEntriesReadThisRun;
FileProcessor(String name,
boolean firstThread,
EnvironmentImpl env,
Cleaner cleaner,
UtilizationProfile profile,
UtilizationCalculator calculator,
FileSelector fileSelector) {
super(0, name, env);
this.cleaner = cleaner;
this.fileSelector = fileSelector;
this.profile = profile;
this.calculator = calculator;
this.firstThread = firstThread;
}
/**
* Return the number of retries when a deadlock exception occurs.
*/
@Override
protected long nDeadlockRetries() {
return cleaner.nDeadlockRetries;
}
void activateOnWakeup() {
activate = true;
}
/**
* The thread is woken, either by an explicit notify (call to {@link
* Cleaner#wakeupActivate()}, or when the timed wakeup interval elapses.
*
* In the former case (a call to wakeupActivate), the 'activate' field will
* be true and the doClean method is called here. This happens when the
* number of bytes written exceeds the cleanerBytesInterval, a config
* change is made that could impact cleaning, etc.
*
* In the latter case (the wakeup interval elapsed), 'activate' will be
* false. In this case, when there has been no writing since the last
* wakeup, we perform cleaning and checkpointing, if needed to reclaim
* space. This handles the situation where writing stops, but cleaning
* or checkpointing or reserved file deletion may be needed. See {@link
* com.sleepycat.je.EnvironmentConfig#CLEANER_WAKEUP_INTERVAL}.
*
* In all cases, when a disk limit is in violation we always call the
* doClean method to ensure that {@link Cleaner#manageDiskUsage()} is
* called in this situation. This is important to free disk space whenever
* possible.
*/
@Override
protected synchronized void onWakeup() {
if (!activate && cleaner.getDiskLimitViolation() == null) {
/*
* This is a timed wakeup and no disk limit is violated. We should
* only call doClean if writing has stopped.
*/
final long nextLsn = envImpl.getFileManager().getNextLsn();
if (lastWakeupLsn != nextLsn) {
/*
* If the last LSN in the log has changed since the last timed
* wakeup, do nothing, because writing has not stopped. As long
* as writing continues, we expect the cleaner and checkpointer
* to be woken via their byte interval params.
*/
lastWakeupLsn = nextLsn;
return;
}
/*
* There has been no writing since the last wakeup. Schedule a
* checkpoint, if needed to reclaim disk space for already cleaned
* files. Then fall through and activate (call doClean).
*/
envImpl.getCheckpointer().wakeupAfterNoWrites();
}
doClean(
true /*invokedFromDaemon*/,
true /*cleanMultipleFiles*/,
false /*forceCleaning*/);
activate = false;
}
/**
* Selects files to clean and cleans them. It returns the number of
* successfully cleaned files. May be called by the daemon thread or
* programatically.
*
* @param invokedFromDaemon currently has no effect.
*
* @param cleanMultipleFiles is true to clean until we're under budget,
* or false to clean at most one file.
*
* @param forceCleaning is true to clean even if we're not under the
* utilization threshold.
*
* @return the number of files cleaned, not including files cleaned
* unsuccessfully.
*/
synchronized int doClean(
boolean invokedFromDaemon,
boolean cleanMultipleFiles,
boolean forceCleaning) {
if (envImpl.isClosed()) {
return 0;
}
/*
* Get all file summaries including tracked files. Tracked files may
* be ready for cleaning if there is a large cache and many files have
* not yet been flushed and do not yet appear in the profile map.
*/
SortedMap fileSummaryMap =
profile.getFileSummaryMap(true /*includeTrackedFiles*/);
/* Clean until no more files are selected. */
final int nOriginalLogFiles = fileSummaryMap.size();
int nFilesCleaned = 0;
while (true) {
/* Stop if the daemon is paused or the environment is closing. */
if ((invokedFromDaemon && isPaused()) || envImpl.isClosing()) {
break;
}
/*
* Manage disk usage (refresh stats and delete files) periodically.
*
* Do this before cleaning, to reduce the chance of filling the
* disk while cleaning and migrating/logging LNs. Also do it after
* cleaning (before deciding whether to clean another file), even
* if there are no more files to clean, to ensure space is freed
* after a long run.
*/
cleaner.manageDiskUsage();
/*
* Stop if we cannot write because of a disk limit violation. */
try {
envImpl.checkDiskLimitViolation();
} catch (DiskLimitException e) {
if (!invokedFromDaemon) {
throw e;
}
break;
}
/*
* Process pending LNs periodically. Pending LNs can prevent file
* deletion.
*/
cleaner.processPending();
if (nFilesCleaned > 0) {
/* If we should only clean one file, stop now. */
if (!cleanMultipleFiles) {
break;
}
/* Don't clean forever. */
if (nFilesCleaned >= nOriginalLogFiles) {
break;
}
/* Refresh file summary info for next file selection. */
fileSummaryMap =
profile.getFileSummaryMap(true /*includeTrackedFiles*/);
}
/*
* Select the next file for cleaning and update the Cleaner's
* read-only file collections.
*/
final Pair result =
fileSelector.selectFileForCleaning(
calculator, fileSummaryMap, forceCleaning);
/* Stop if no file is selected for cleaning. */
if (result == null) {
/*
* Process pending LNs periodically when no files are being
* cleaned.
*/
cleaner.processPending();
break;
}
final Long fileNum = result.first();
final int requiredUtil = result.second();
final boolean twoPass = (requiredUtil >= 0);
boolean finished = false;
boolean fileDeleted = false;
final long fileNumValue = fileNum;
final long runId = cleaner.totalRuns.incrementAndGet();
final MemoryBudget budget = envImpl.getMemoryBudget();
nFilesCleaned += 1;
try {
TestHookExecute.doHookIfSet(cleaner.fileChosenHook);
/* Perform 1st pass of 2-pass cleaning. */
String passOneMsg = "";
if (twoPass) {
final FileSummary recalcSummary = new FileSummary();
final ExpirationTracker expTracker =
new ExpirationTracker(fileNumValue);
processFile(
fileNum, recalcSummary, new INSummary(), expTracker);
final int expiredSize =
expTracker.getExpiredBytes(TTL.currentSystemTime());
final int obsoleteSize = recalcSummary.getObsoleteSize();
final int recalcUtil = FileSummary.utilization(
obsoleteSize + expiredSize, recalcSummary.totalSize);
passOneMsg =
" pass1RecalcObsolete=" + obsoleteSize +
" pass1RecalcExpired=" + expiredSize +
" pass1RecalcUtil=" + recalcUtil +
" pass1RequiredUtil=" + requiredUtil;
if (recalcUtil > requiredUtil) {
cleaner.nRevisalRuns.increment();
cleaner.getExpirationProfile().putFile(
expTracker, expiredSize);
final String logMsg = "CleanerRevisalRun " + runId +
" on file 0x" + Long.toHexString(fileNumValue) +
" ends:" + passOneMsg;
LoggerUtils.logMsg(logger, envImpl, Level.INFO, logMsg);
fileSelector.removeFile(fileNum, budget);
finished = true;
continue;
}
}
resetPerRunCounters();
cleaner.nCleanerRuns.increment();
if (twoPass) {
cleaner.nTwoPassRuns.increment();
}
/* Keep track of estimated and true utilization. */
final FileSummary estimatedFileSummary =
fileSummaryMap.containsKey(fileNum) ?
fileSummaryMap.get(fileNum).clone() : null;
final FileSummary recalculatedFileSummary = new FileSummary();
final INSummary inSummary = new INSummary();
final String msgHeader =
(twoPass ? "CleanerTwoPassRun " : "CleanerRun ") +
runId + " on file 0x" + Long.toHexString(fileNumValue);
final String beginMsg = msgHeader + " begins:";
/* Trace is unconditional for log-based debugging. */
LoggerUtils.traceAndLog(logger, envImpl, Level.FINE, beginMsg);
/* Process all log entries in the file. */
if (!processFile(
fileNum, recalculatedFileSummary, inSummary, null)) {
return nFilesCleaned;
}
/* File is fully processed, update stats. */
accumulatePerRunCounters();
finished = true;
/* Trace is unconditional for log-based debugging. */
final String endMsg = msgHeader + " ends:" +
" invokedFromDaemon=" + invokedFromDaemon +
" finished=" + finished +
" fileDeleted=" + fileDeleted +
" nEntriesRead=" + nEntriesReadThisRun +
" nDbLookups=" + nDbLookupsThisRun +
" nINsObsolete=" + nINsObsoleteThisRun +
" nINsCleaned=" + nINsCleanedThisRun +
" nINsDead=" + nINsDeadThisRun +
" nINsMigrated=" + nINsMigratedThisRun +
" nBINDeltasObsolete=" + nBINDeltasObsoleteThisRun +
" nBINDeltasCleaned=" + nBINDeltasCleanedThisRun +
" nBINDeltasDead=" + nBINDeltasDeadThisRun +
" nBINDeltasMigrated=" + nBINDeltasMigratedThisRun +
" nLNsObsolete=" + nLNsObsoleteThisRun +
" nLNsCleaned=" + nLNsCleanedThisRun +
" nLNsDead=" + nLNsDeadThisRun +
" nLNsExpired=" + nLNsExpiredThisRun +
" nLNsExtinct=" + nLNsExtinctThisRun +
" nLNsMigrated=" + nLNsMigratedThisRun +
" nLNsMarked=" + nLNsMarkedThisRun +
" nLNQueueHits=" + nLNQueueHitsThisRun +
" nLNsLocked=" + nLNsLockedThisRun;
Trace.trace(envImpl, endMsg);
/* Only construct INFO level message if needed. */
if (logger.isLoggable(Level.INFO)) {
final int estUtil = (estimatedFileSummary != null) ?
estimatedFileSummary.utilization() : -1;
final int recalcUtil =
recalculatedFileSummary.utilization();
LoggerUtils.logMsg(
logger, envImpl, Level.INFO,
endMsg +
" inSummary=" + inSummary +
" estSummary=" + estimatedFileSummary +
" recalcSummary=" + recalculatedFileSummary +
" estimatedUtil=" + estUtil +
" recalcUtil=" + recalcUtil +
passOneMsg);
}
} catch (FileNotFoundException e) {
/*
* File was deleted. Although it is possible that the file was
* deleted externally it is much more likely that the file was
* deleted normally after being cleaned earlier. This can
* occur when tracked obsolete information is collected and
* processed after the file has been cleaned and deleted.
* Since the file does not exist, ignore the error so that the
* cleaner will continue. Remove the file completely from the
* FileSelector, UtilizationProfile and ExpirationProfile so
* that we don't repeatedly attempt to process it. [#15528]
*/
fileDeleted = true;
profile.removeDeletedFile(fileNum);
cleaner.getExpirationProfile().removeFile(fileNum);
fileSelector.removeFile(fileNum, budget);
LoggerUtils.logMsg(
logger, envImpl, Level.INFO,
"Missing file 0x" + Long.toHexString(fileNum) +
" ignored by cleaner");
} catch (IOException e) {
LoggerUtils.traceAndLogException(
envImpl, "Cleaner", "doClean", "", e);
throw new EnvironmentFailureException(
envImpl, EnvironmentFailureReason.LOG_INTEGRITY, e);
} catch (DiskLimitException e) {
LoggerUtils.logMsg(
logger, envImpl, Level.WARNING,
"Cleaning of file 0x" + Long.toHexString(fileNum) +
" aborted because of disk limit violation: " + e);
if (!invokedFromDaemon) {
throw e;
}
} catch (RuntimeException e) {
LoggerUtils.traceAndLogException(
envImpl, "Cleaner", "doClean", "", e);
throw e;
} finally {
if (!finished && !fileDeleted) {
fileSelector.putBackFileForCleaning(fileNum);
}
}
}
return nFilesCleaned;
}
/**
* Calculates expired bytes without performing any migration or other side
* effects. The expired sizes will not overlap with obsolete data, because
* expired sizes are accumulated only for non-obsolete entries.
*
* @param fileNum file to read.
*
* @return the expiration tracker.
*/
public ExpirationTracker countExpiration(long fileNum) {
final ExpirationTracker tracker = new ExpirationTracker(fileNum);
try {
final boolean result = processFile(
fileNum, new FileSummary(), new INSummary(), tracker);
assert result;
} catch (IOException e) {
LoggerUtils.traceAndLogException(
envImpl, "Cleaner", "countExpiration", "", e);
throw new EnvironmentFailureException(
envImpl, EnvironmentFailureReason.LOG_INTEGRITY, e);
}
return tracker;
}
/**
* Process all log entries in the given file.
*
* Note that we gather obsolete offsets at the beginning of the method and
* do not check for obsolete offsets of entries that become obsolete while
* the file is being processed. An entry in this file can become obsolete
* before we process it when normal application activity deletes or
* updates the entry. Also, large numbers of entries also become obsolete
* as the result of LN migration while processing the file, but these
* Checking the TrackedFileSummary while processing the file would be
* expensive if it has many entries, because we perform a linear search in
* the TFS. There is a tradeoff between the cost of the TFS lookup and its
* benefit, which is to avoid a tree search if the entry is obsolete. Many
* more lookups for non-obsolete entries than obsolete entries will
* typically be done. Because of the high cost of the linear search,
* especially when processing large log files, we do not check the TFS.
* [#19626]
*
* In countOnly mode (expTracker != null), expiration info is returned
* via the expTracker param, obsolete info returned via fileSummary does
* not include expired data, and no migration is performed, i.e., there
* are no side effects. Also, checksums are not verified because counting
* is a non-destructive operation and often redundant, since it is used
* for two pass cleaning and reading files in the recovery interval. It
* is particularly important to avoid checksum verification during
* recovery since it adds significantly to overall recovery time.
*
* @param fileNum the file being cleaned.
*
* @param fileSummary used to return the true utilization.
*
* @param inSummary used to return IN utilization info for debugging.
*
* @param expTracker if non-null, enables countOnly mode.
*
* @return false if we aborted file processing because the environment is
* being closed.
*/
private boolean processFile(Long fileNum,
FileSummary fileSummary,
INSummary inSummary,
ExpirationTracker expTracker)
throws IOException {
final boolean countOnly = (expTracker != null);
final LockManager lockManager =
envImpl.getTxnManager().getLockManager();
/* Get the current obsolete offsets for this file. */
final PackedOffsets obsoleteOffsets = profile.getObsoleteDetailPacked(
fileNum, !countOnly /*logUpdate*/, null);
final PackedOffsets.Iterator obsoleteIter = obsoleteOffsets.iterator();
long nextObsolete = -1;
/* Copy to local variables because they are mutable properties. */
final int readBufferSize = cleaner.readBufferSize;
final int lookAheadCacheSize =
countOnly ? 0 : cleaner.lookAheadCacheSize;
/*
* Add the overhead of this method to the budget. Two read buffers are
* allocated by the file reader. The log size of the offsets happens to
* be the same as the memory overhead.
*/
final int adjustMem = (2 * readBufferSize) +
obsoleteOffsets.getLogSize() +
lookAheadCacheSize;
final MemoryBudget budget = envImpl.getMemoryBudget();
budget.updateAdminMemoryUsage(adjustMem);
/* Evict after updating the budget. */
if (Cleaner.DO_CRITICAL_EVICTION) {
envImpl.daemonEviction(true /*backgroundIO*/);
}
/*
* We keep a look ahead cache of non-obsolete LNs. When we lookup a
* BIN in processLN, we also process any other LNs in that BIN that are
* in the cache. This can reduce the number of tree lookups.
*/
final LookAheadCache lookAheadCache =
countOnly ? null : new LookAheadCache(lookAheadCacheSize);
/* Use local caching to reduce DbTree.getDb calls. */
final DbCache dbCache = new DbCache(envImpl, cleaner);
/*
* Expired entries are counted obsolete so that this is reflected in
* total utilization. A separate tracker is used so it can be added in
* a single call under the log write latch.
*/
final LocalUtilizationTracker localTracker =
countOnly ? null : new LocalUtilizationTracker(envImpl);
/* Keep track of all database IDs encountered. */
final Set databases = new HashSet<>();
/* Create the file reader. */
final CleanerFileReader reader = new CleanerFileReader(
envImpl, readBufferSize, DbLsn.makeLsn(fileNum, 0), fileNum,
fileSummary, inSummary, expTracker);
/* Validate all entries before ever deleting a file. */
reader.setAlwaysValidateChecksum(true);
try {
final TreeLocation location = new TreeLocation();
int nProcessedEntries = 0;
while (reader.readNextEntryAllowExceptions()) {
nProcessedEntries += 1;
cleaner.nEntriesRead.increment();
int nReads = reader.getAndResetNReads();
if (nReads > 0) {
cleaner.nDiskReads.add(nReads);
}
long logLsn = reader.getLastLsn();
long fileOffset = DbLsn.getFileOffset(logLsn);
boolean isLN = reader.isLN();
boolean isIN = reader.isIN();
boolean isBINDelta = reader.isBINDelta();
boolean isOldBINDelta = reader.isOldBINDelta();
boolean isDbTree = reader.isDbTree();
boolean isObsolete = false;
long expirationTime = 0;
/* Remember the version of the log file. */
if (reader.isFileHeader()) {
fileLogVersion = reader.getFileHeader().getLogVersion();
/* No expiration info exists before version 12. */
if (countOnly && fileLogVersion < 12) {
return true; // TODO caller must abort also
}
}
/* Stop if the daemon is shut down. */
if (!countOnly && envImpl.isClosing()) {
return false;
}
/* Exit loop if we can't write. */
if (!countOnly) {
envImpl.checkDiskLimitViolation();
}
/* Update background reads. */
if (nReads > 0) {
envImpl.updateBackgroundReads(nReads);
}
/* Sleep if background read/write limit was exceeded. */
envImpl.sleepAfterBackgroundIO();
/* Check for a known obsolete node. */
while (nextObsolete < fileOffset && obsoleteIter.hasNext()) {
nextObsolete = obsoleteIter.next();
}
if (nextObsolete == fileOffset) {
isObsolete = true;
}
/* Check for the entry type next because it is very cheap. */
if (!isObsolete &&
!isLN &&
!isIN &&
!isBINDelta &&
!isOldBINDelta &&
!isDbTree) {
/* Consider other entries (the file header) obsolete. */
assert reader.isFileHeader();
isObsolete = true;
}
/*
* Ignore deltas before log version 8. Before the change to
* place deltas in the Btree (in JE 5.0), all deltas were
* considered obsolete by the cleaner. Processing an old delta
* would be very wasteful (a Btree lookup, and possibly
* dirtying and flushing a BIN), and for duplicates databases
* could cause an exception due to the key format change.
* [#21405]
*/
if (!isObsolete &&
isOldBINDelta &&
fileLogVersion < 8) {
isObsolete = true;
}
/* Maintain a set of all databases encountered. */
final DatabaseId dbId = reader.getDatabaseId();
if (dbId != null) {
databases.add(dbId);
}
/*
* Get database. This is postponed until we need it, to reduce
* contention in DbTree.getDb.
*/
DbInfo dbInfo = null;
if (!isObsolete && dbId != null) {
/*
* Release cached dbImpls after dbCacheClearCount entries,
* to prevent starving other threads that need exclusive
* access to the MapLN (for example, DbTree.deleteMapLN).
* [#21015]
*/
if ((nProcessedEntries % cleaner.dbCacheClearCount) == 0) {
dbCache.releaseDbImpls();
}
/*
* Get DB info needed for checking obsolescence. The
* static DbInfo fields (dups, name, etc) are cached even
* after the DB is released by the periodic calls to
* releaseDbImpls above. These static fields can be used
* to determine obsolescence in many cases.
*
* If the DB was deleted at the time of its last lookup,
* the deleting or deleted field will be true and we
* check this condition here. However, due to the
* releaseDbImpls call above, the DatabaseImpl may have
* been released and another thread may delete the DB
* even when these fields are both false. So before
* migrating an entry we think is not obsolete (further
* below) we must call getDbImpl() and check this
* condition again, to ensure we do not migrate an entry
* concurrently with DB deletion.
*/
dbInfo = dbCache.getDbInfo(dbId);
if (dbInfo.deleting || dbInfo.deleted) {
isObsolete = true;
}
}
/*
* Also ignore INs in dup DBs before log version 8. These must
* be obsolete, just as DINs and DBINs must be obsolete (and
* are also ignored here) after dup DB conversion. Also, the
* old format IN key cannot be used for lookups. [#21405]
*/
if (!isObsolete &&
isIN &&
dbInfo.dups &&
fileLogVersion < 8) {
isObsolete = true;
}
if (!isObsolete && isLN) {
final LNLogEntry> lnEntry = reader.getLNLogEntry();
lnEntry.postFetchInit(dbInfo.dups);
/*
* SR 14583: In JE 2.0 and later we can assume that all
* deleted LNs are obsolete. Either the delete committed
* and the BIN parent is marked with a pending deleted bit,
* or the delete rolled back, in which case there is no
* reference to this entry. JE 1.7.1 and earlier require a
* tree lookup because deleted LNs may still be reachable
* through their BIN parents.
*/
if (lnEntry.isDeleted() && fileLogVersion > 2) {
isObsolete = true;
}
/* "Immediately obsolete" LNs can be discarded. */
if (!isObsolete &&
(dbInfo.isLNImmediatelyObsolete ||
lnEntry.isEmbeddedLN())) {
isObsolete = true;
}
/*
* Check for expired LN. If locked, add to pending queue.
*/
if (!isObsolete && !countOnly) {
expirationTime = TTL.expirationToSystemTime(
lnEntry.getExpiration(),
lnEntry.isExpirationInHours());
if (envImpl.expiresWithin(
expirationTime,
0 - envImpl.getTtlLnPurgeDelay())) {
if (!lockManager.isLockUncontended(logLsn)) {
fileSelector.addPendingLN(
logLsn,
new LNInfo(null /*LN*/, dbId,
lnEntry.getKey(), expirationTime));
nLNsLockedThisRun++;
continue;
}
isObsolete = true;
nLNsExpiredThisRun += 1;
/*
* Inexact counting is used to avoid overhead of
* adding obsolete offset.
*/
localTracker.countObsoleteNodeInexact(
logLsn, null /*type*/,
reader.getLastEntrySize());
}
}
/* Check for extinct LN. */
if (!isObsolete &&
envImpl.getExtinctionState(
dbInfo.name, dbInfo.dups, dbInfo.internal,
lnEntry.getKey()) == EXTINCT) {
isObsolete = true;
nLNsExtinctThisRun += 1;
}
}
/* Skip known obsolete nodes. */
if (isObsolete) {
/* Count obsolete stats. */
if (!countOnly) {
if (isLN) {
nLNsObsoleteThisRun++;
} else if (isBINDelta || isOldBINDelta) {
nBINDeltasObsoleteThisRun++;
} else if (isIN) {
nINsObsoleteThisRun++;
}
}
/* Count utilization for obsolete entry. */
reader.countObsolete();
continue;
}
/* If not obsolete, count expired. */
reader.countExpired();
/* Don't process further if we are only calculating. */
if (countOnly) {
continue;
}
/* Evict before processing each entry. */
if (Cleaner.DO_CRITICAL_EVICTION) {
envImpl.daemonEviction(true /*backgroundIO*/);
}
/* The entry is not known to be obsolete -- process it now. */
assert lookAheadCache != null;
if (isLN) {
final LNLogEntry> lnEntry = reader.getLNLogEntry();
final LN targetLN = lnEntry.getLN();
final byte[] key = lnEntry.getKey();
/*
* Note that the final check for a deleted DB is
* performed in processLN.
*/
lookAheadCache.add(
DbLsn.getFileOffset(logLsn),
new LNInfo(targetLN, dbId, key, expirationTime));
if (lookAheadCache.isFull()) {
processLN(fileNum, location, lookAheadCache, dbCache);
}
} else if (isDbTree) {
envImpl.rewriteMapTreeRoot(logLsn);
} else {
/*
* Do the final check for a deleted DB, prior to
* processing (and potentially migrating) an IN.
*/
dbInfo = dbCache.getDbImpl(dbId);
if (dbInfo.deleted || dbInfo.deleting) {
/*
* If the DB has been deleted, perform the
* housekeeping tasks for an obsolete IN.
*/
if (isBINDelta || isOldBINDelta) {
nBINDeltasObsoleteThisRun++;
} else if (isIN) {
nINsObsoleteThisRun++;
}
} else if (isIN) {
final DatabaseImpl db = dbInfo.dbImpl;
final IN targetIN = reader.getIN(db);
targetIN.setDatabase(db);
processIN(targetIN, db, logLsn);
} else if (isOldBINDelta) {
final OldBINDelta delta = reader.getOldBINDelta();
processOldBINDelta(delta, dbInfo.dbImpl, logLsn);
} else if (isBINDelta) {
final BIN delta = reader.getBINDelta();
processBINDelta(delta, dbInfo.dbImpl, logLsn);
} else {
assert false;
}
}
}
/* Don't process further if we are only calculating. */
if (countOnly) {
return true;
}
/* Process remaining queued LNs. */
while (!lookAheadCache.isEmpty()) {
if (Cleaner.DO_CRITICAL_EVICTION) {
envImpl.daemonEviction(true /*backgroundIO*/);
}
processLN(fileNum, location, lookAheadCache, dbCache);
/* Sleep if background read/write limit was exceeded. */
envImpl.sleepAfterBackgroundIO();
}
/*
* Update the pending DB set for each DB previously found in the
* 'deleting' state. It is not worthwhile to call getDbImpl here
* to check whether DB state has changed from deleting to deleted.
* This would increase the number of DbTree.getDb calls per
* cleaner run. DbTree.getDb will be called again when processing
* the pending DBs.
*/
for (Map.Entry entry : dbCache) {
if (entry.getValue().deleting) {
cleaner.addPendingDB(fileNum, entry.getKey());
}
}
/* Update per-run stats. */
nEntriesReadThisRun = reader.getNumRead();
nDbLookupsThisRun = dbCache.getLookups();
/* This will flush just the one FSLN for this file. */
envImpl.getUtilizationProfile().flushLocalTracker(localTracker);
} catch (ChecksumException e) {
throw new EnvironmentFailureException
(envImpl, EnvironmentFailureReason.LOG_CHECKSUM, e);
} finally {
/* Subtract the overhead of this method from the budget. */
budget.updateAdminMemoryUsage(0 - adjustMem);
/* Release all cached DBs. */
dbCache.releaseDbImpls();
}
/* File is fully processed, update status information. */
fileSelector.addCleanedFile(
fileNum, databases, reader.getFirstVLSN(), reader.getLastVLSN(),
budget);
/*
* Attempt to process any pending LNs/DBs that were added while
* cleaning.
*/
cleaner.processPending();
return true;
}
/**
* Processes the first LN in the look ahead cache and removes it from the
* cache. While the BIN is latched, look through the BIN for other LNs in
* the cache; if any match, process them to avoid a tree search later.
*/
private void processLN(
final Long fileNum,
final TreeLocation location,
final LookAheadCache lookAheadCache,
final DbCache dbCache) {
/* Get the first LN from the queue. */
final Long offset = lookAheadCache.nextOffset();
final LNInfo info = lookAheadCache.remove(offset);
final LN lnFromLog = info.getLN();
final byte[] keyFromLog = info.getKey();
final long logLsn = DbLsn.makeLsn(fileNum, offset);
/*
* Do the final check for a deleted DB, prior to processing (and
* potentially migrating) the LN. If the DB has been deleted,
* perform the housekeeping tasks for an obsolete LN.
*/
final DatabaseId dbId = info.getDbId();
final DbInfo dbInfo = dbCache.getDbImpl(dbId);
if (dbInfo.deleted || dbInfo.deleting) {
nLNsObsoleteThisRun++;
return;
}
final DatabaseImpl db = dbInfo.dbImpl;
nLNsCleanedThisRun++;
/* Status variables are used to generate debug tracing info. */
boolean processedHere = true; // The LN was cleaned here.
boolean obsolete = false; // The LN is no longer in use.
boolean completed = false; // This method completed.
BIN bin = null;
Map pendingLNs = null;
try {
final Tree tree = db.getTree();
assert tree != null;
/* Find parent of this LN. */
final boolean parentFound = tree.getParentBINForChildLN(
location, keyFromLog, false /*splitsAllowed*/,
false /*blindDeltaOps*/, Cleaner.UPDATE_GENERATION);
bin = location.bin;
final int index = location.index;
if (!parentFound) {
nLNsDeadThisRun++;
obsolete = true;
completed = true;
return;
}
/*
* Now we're at the BIN parent for this LN. If knownDeleted, LN is
* deleted and can be purged.
*/
if (bin.isEntryKnownDeleted(index)) {
nLNsDeadThisRun++;
obsolete = true;
completed = true;
return;
}
/* Process this LN that was found in the tree. */
processedHere = false;
LNInfo pendingLN =
processFoundLN(info, logLsn, bin.getLsn(index), bin, index);
if (pendingLN != null) {
pendingLNs = new HashMap<>();
pendingLNs.put(logLsn, pendingLN);
}
completed = true;
/*
* For all other non-deleted LNs in this BIN, lookup their LSN
* in the LN queue and process any matches.
*/
for (int i = 0; i < bin.getNEntries(); i += 1) {
final long binLsn = bin.getLsn(i);
if (i != index &&
!bin.isEntryKnownDeleted(i) &&
!bin.isEntryPendingDeleted(i) &&
DbLsn.getFileNumber(binLsn) == fileNum) {
final Long myOffset = DbLsn.getFileOffset(binLsn);
final LNInfo myInfo = lookAheadCache.remove(myOffset);
/* If the offset is in the cache, it's a match. */
if (myInfo != null) {
nLNQueueHitsThisRun++;
nLNsCleanedThisRun++;
pendingLN =
processFoundLN(myInfo, binLsn, binLsn, bin, i);
if (pendingLN != null) {
if (pendingLNs == null) {
pendingLNs = new HashMap<>();
}
pendingLNs.put(binLsn, pendingLN);
}
}
}
}
} finally {
if (bin != null) {
bin.releaseLatch();
}
/* BIN must not be latched when synchronizing on FileSelector. */
if (pendingLNs != null) {
for (Map.Entry entry : pendingLNs.entrySet()) {
fileSelector.addPendingLN(
entry.getKey(), entry.getValue());
}
}
if (processedHere) {
cleaner.logFine(Cleaner.CLEAN_LN, lnFromLog, logLsn,
completed, obsolete, false /*migrated*/);
}
}
}
/**
* Processes an LN that was found in the tree. Lock the LN's LSN and
* then migrates the LN, if the LSN of the LN log entry is the active LSN
* in the tree.
*
* @param info identifies the LN log entry.
*
* @param logLsn is the LSN of the log entry.
*
* @param treeLsn is the LSN found in the tree.
*
* @param bin is the BIN found in the tree; is latched on method entry and
* exit.
*
* @param index is the BIN index found in the tree.
*
* @return a non-null LNInfo if it should be added to the pending LN list,
* after releasing the BIN latch.
*/
private LNInfo processFoundLN(
final LNInfo info,
final long logLsn,
final long treeLsn,
final BIN bin,
final int index) {
final LN lnFromLog = info.getLN();
final byte[] key = info.getKey();
final DatabaseImpl db = bin.getDatabase();
final boolean isTemporary = db.isTemporary();
/* Status variables are used to generate debug tracing info. */
boolean obsolete = false; // The LN is no longer in use.
boolean migrated = false; // The LN was in use and is migrated.
boolean completed = false; // This method completed.
BasicLocker locker = null;
try {
final Tree tree = db.getTree();
assert tree != null;
/*
* Before migrating an LN, we must lock it and then check to see
* whether it is obsolete or active.
*
* 1. If the LSN in the tree and in the log are the same, we will
* attempt to migrate it.
*
* 2. If the LSN in the tree is < the LSN in the log, the log entry
* is obsolete, because this LN has been rolled back to a previous
* version by a txn that aborted.
*
* 3. If the LSN in the tree is > the LSN in the log, the log entry
* is obsolete, because the LN was advanced forward by some
* now-committed txn.
*
* 4. If the LSN in the tree is a null LSN, the log entry is
* obsolete. A slot can only have a null LSN if the record has
* never been written to disk in a deferred write database, and
* in that case the log entry must be for a past, deleted version
* of that record.
*/
if (lnFromLog.isDeleted() &&
treeLsn == logLsn &&
fileLogVersion <= 2) {
/*
* SR 14583: After JE 2.0, deleted LNs are never found in the
* tree, since we can assume they're obsolete and correctly
* marked as such in the obsolete offset tracking. JE 1.7.1 and
* earlier did not use the pending deleted bit, so deleted LNs
* may still be reachable through their BIN parents.
*/
obsolete = true;
nLNsDeadThisRun++;
bin.setPendingDeleted(index);
completed = true;
return null;
}
if (treeLsn == DbLsn.NULL_LSN) {
/*
* Case 4: The LN in the tree is a never-written LN for a
* deferred-write db, so the LN in the file is obsolete.
*/
nLNsDeadThisRun++;
obsolete = true;
completed = true;
return null;
}
if (treeLsn != logLsn && isTemporary) {
/*
* Temporary databases are always non-transactional. If the
* tree and log LSNs are different then we know that the logLsn
* is obsolete. Even if the LN is locked, the tree cannot be
* restored to the logLsn because no abort is possible without
* a transaction. We should consider a similar optimization in
* the future for non-transactional durable databases.
*/
nLNsDeadThisRun++;
obsolete = true;
completed = true;
return null;
}
if (!isTemporary) {
/*
* Get a lock on the LN if we will migrate it now. (Temporary
* DB LNs are dirtied below and migrated later.)
*
* We can hold the latch on the BIN since we always attempt to
* acquire a non-blocking read lock.
*/
locker = BasicLocker.createBasicLocker(envImpl, false /*noWait*/);
/* Don't allow this short-lived lock to be preempted/stolen. */
locker.setPreemptable(false);
final LockResult lockRet = locker.nonBlockingLock(
treeLsn, LockType.READ, false /*jumpAheadOfWaiters*/, db);
if (lockRet.getLockGrant() == LockGrantType.DENIED) {
/*
* LN is currently locked by another Locker, so we can't
* assume anything about the value of the LSN in the bin.
*/
nLNsLockedThisRun++;
completed = true;
return new LNInfo(
null /*LN*/, db.getId(), key,
info.getExpirationTime());
}
if (treeLsn != logLsn) {
/* The LN is obsolete and can be purged. */
nLNsDeadThisRun++;
obsolete = true;
completed = true;
return null;
}
}
/*
* The LN must be migrated because it is not obsolete, the lock was
* not denied, and treeLsn==logLsn.
*/
assert !obsolete;
assert treeLsn == logLsn;
if (bin.isEmbeddedLN(index)) {
throw EnvironmentFailureException.unexpectedState(
envImpl,
"LN is embedded although its associated logrec (at " +
logLsn + " does not have the embedded flag on");
}
/*
* For active LNs in non-temporary DBs, migrate the LN now.
* In this case we acquired a lock on the LN above.
*
* If the LN is not resident, populate it using the LN we read
* from the log so it does not have to be fetched. We must
* call postFetchInit to initialize MapLNs that have not been
* fully initialized yet [#13191]. When explicitly migrating
* (for a non-temporary DB) we will evict the LN after logging.
*
* Note that we do not load LNs from the off-heap cache here
* because it's unnecessary. We have the current LN in hand
* (from the log) and the off-heap cache does not hold dirty
* LNs, so the IN in hand is identical to the off-heap LN.
*
* MapLNs must be logged by DbTree.modifyDbRoot (the Tree root
* latch must be held) [#23492]. Here we simply dirty it via
* setDirty, which ensures it will be logged during the next
* checkpoint. Delaying until the next checkpoint also allows
* for write absorption, since MapLNs are often logged every
* checkpoint due to utilization changes.
*
* For temporary databases, we wish to defer logging for as
* long as possible. Therefore, dirty the LN to ensure it is
* flushed before its parent is written. Because we do not
* attempt to lock temporary database LNs (see above) we know
* that if it is non-obsolete, the tree and log LSNs are equal.
* If the LN from the log was populated here, it will be left
* in place for logging at a later time.
*
* Also for temporary databases, make both the target LN and
* the BIN or IN parent dirty. Otherwise, when the BIN or IN is
* evicted in the future, it will be written to disk without
* flushing its dirty, migrated LNs. [#18227]
*/
if (bin.getTarget(index) == null) {
lnFromLog.postFetchInit(db, logLsn);
/* Ensure keys are transactionally correct. [#15704] */
bin.attachNode(index, lnFromLog, key /*lnSlotKey*/);
}
if (db.getId().equals(DbTree.ID_DB_ID)) {
final MapLN targetLn = (MapLN) bin.getTarget(index);
assert targetLn != null;
targetLn.getDatabase().setDirty();
} else if (isTemporary) {
((LN) bin.getTarget(index)).setDirty();
bin.setDirty(true);
nLNsMarkedThisRun++;
} else {
final LN targetLn = (LN) bin.getTarget(index);
assert targetLn != null;
final LogItem logItem = targetLn.log(
envImpl, db, null /*locker*/, null /*writeLockInfo*/,
false /*newEmbeddedLN*/, bin.getKey(index),
bin.getExpiration(index), bin.isExpirationInHours(),
false /*newEmbeddedLN*/, logLsn,
bin.getLastLoggedSize(index),
false/*isInsertion*/, true /*backgroundIO*/,
Cleaner.getMigrationRepContext(targetLn));
bin.updateEntry(
index, logItem.lsn, targetLn.getVLSNSequence(),
logItem.size);
/* Evict LN if we populated it with the log LN. */
if (lnFromLog == targetLn) {
bin.evictLN(index);
}
/* Lock new LSN on behalf of existing lockers. */
CursorImpl.lockAfterLsnChange(
db, logLsn, logItem.lsn, locker /*excludeLocker*/);
nLNsMigratedThisRun++;
}
migrated = true;
completed = true;
return null;
} finally {
if (locker != null) {
locker.operationEnd();
}
cleaner.logFine(Cleaner.CLEAN_LN, lnFromLog, logLsn, completed,
obsolete, migrated);
}
}
/**
* If this OldBINDelta is still in use in the in-memory tree, dirty the
* associated BIN. The next checkpoint will log a new delta or a full
* version, which will make this delta obsolete.
*
* For OldBINDeltas, we do not optimize and must fetch the BIN if it is not
* resident.
*/
private void processOldBINDelta(
OldBINDelta deltaClone,
DatabaseImpl db,
long logLsn) {
nBINDeltasCleanedThisRun++;
/*
* Search Btree for the BIN associated with this delta.
*/
final byte[] searchKey = deltaClone.getSearchKey();
final BIN treeBin = db.getTree().search(
searchKey, Cleaner.UPDATE_GENERATION);
if (treeBin == null) {
/* BIN for this delta is no longer in the tree. */
nBINDeltasDeadThisRun++;
return;
}
/* Tree BIN is non-null and latched. */
try {
final long treeLsn = treeBin.getLastLoggedLsn();
if (treeLsn == DbLsn.NULL_LSN) {
/* Current version was never logged. */
nBINDeltasDeadThisRun++;
return;
}
final int cmp = DbLsn.compareTo(treeLsn, logLsn);
if (cmp > 0) {
/* Log entry is obsolete. */
nBINDeltasDeadThisRun++;
return;
}
/*
* Log entry is same or newer than what's in the tree. Dirty the
* BIN and let the checkpoint write it out. There is no need to
* prohibit a delta when the BIN is next logged (as is done when
* migrating full INs) because logging a new delta will obsolete
* this delta.
*/
treeBin.setDirty(true);
nBINDeltasMigratedThisRun++;
} finally {
treeBin.releaseLatch();
}
}
/**
* If this BIN-delta is still in use in the in-memory tree, dirty the
* associated BIN. The next checkpoint will log a new delta or a full
* version, which will make this delta obsolete.
*
* We optimize by placing the delta from the log into the tree when the
* BIN is not resident.
*/
private void processBINDelta(
BIN deltaClone,
DatabaseImpl db,
long logLsn) {
nBINDeltasCleanedThisRun++;
/* Search for the BIN's parent by level, to avoid fetching the BIN. */
deltaClone.setDatabase(db);
deltaClone.latch(CacheMode.UNCHANGED);
final SearchResult result = db.getTree().getParentINForChildIN(
deltaClone, true /*useTargetLevel*/,
true /*doFetch*/, CacheMode.UNCHANGED);
try {
if (!result.exactParentFound) {
/* BIN for this delta is no longer in the tree. */
nBINDeltasDeadThisRun++;
return;
}
final long treeLsn = result.parent.getLsn(result.index);
if (treeLsn == DbLsn.NULL_LSN) {
/* Current version was never logged. */
nBINDeltasDeadThisRun++;
return;
}
/*
* If cmp is > 0 then log entry is obsolete because it is older
* than the version in the tree.
*
* If cmp is < 0 then log entry is also obsolete, because the old
* parent slot was deleted and we're now looking at a completely
* different IN due to the by-level search above.
*/
final int cmp = DbLsn.compareTo(treeLsn, logLsn);
if (cmp != 0) {
/* Log entry is obsolete. */
nBINDeltasDeadThisRun++;
return;
}
/*
* Log entry is the version that's in the tree. Dirty the BIN and
* let the checkpoint write it out. There is no need to prohibit a
* delta when the BIN is next logged (as is done when migrating
* full BINs) because logging a new delta will obsolete this delta.
*/
BIN treeBin = (BIN) result.parent.loadIN(
result.index, CacheMode.UNCHANGED);
if (treeBin == null) {
/* Place delta from log into tree to avoid fetching. */
treeBin = deltaClone;
treeBin.latchNoUpdateLRU(db);
treeBin.postFetchInit(db, logLsn);
result.parent.attachNode(
result.index, treeBin, null /*lnSlotKey*/);
} else {
treeBin.latch(CacheMode.UNCHANGED);
}
/*
* Compress to reclaim space for expired slots, including dirty
* slots. However, if treeBin is a BIN-delta, this does nothing.
*/
envImpl.lazyCompress(treeBin, true /*compressDirtySlots*/);
treeBin.setDirty(true);
treeBin.releaseLatch();
nBINDeltasMigratedThisRun++;
} finally {
if (result.parent != null) {
result.parent.releaseLatch();
}
}
}
/**
* If an IN is still in use in the in-memory tree, dirty it. The checkpoint
* invoked at the end of the cleaning run will end up rewriting it.
*/
private void processIN(
IN inClone,
DatabaseImpl db,
long logLsn) {
boolean obsolete = false;
boolean dirtied = false;
boolean completed = false;
try {
nINsCleanedThisRun++;
Tree tree = db.getTree();
assert tree != null;
IN inInTree = findINInTree(tree, db, inClone, logLsn);
if (inInTree == null) {
/* IN is no longer in the tree. Do nothing. */
nINsDeadThisRun++;
obsolete = true;
} else {
/*
* IN is still in the tree. Dirty it. Checkpoint or eviction
* will write it out.
*
* Prohibit the next delta, since the original version must be
* made obsolete.
*
* Compress to reclaim space for expired slots, including dirty
* slots.
*/
nINsMigratedThisRun++;
inInTree.setDirty(true);
inInTree.setProhibitNextDelta(true);
envImpl.lazyCompress(inInTree, true /*compressDirtySlots*/);
inInTree.releaseLatch();
dirtied = true;
}
completed = true;
} finally {
cleaner.logFine(Cleaner.CLEAN_IN, inClone, logLsn, completed,
obsolete, dirtied);
}
}
/**
* Given a clone of an IN that has been taken out of the log, try to find
* it in the tree and verify that it is the current one in the log.
* Returns the node in the tree if it is found and it is current re: LSN's.
* Otherwise returns null if the clone is not found in the tree or it's not
* the latest version. Caller is responsible for unlatching the returned
* IN.
*/
private IN findINInTree(
Tree tree,
DatabaseImpl db,
IN inClone,
long logLsn) {
/* Check if inClone is the root. */
if (inClone.isRoot()) {
IN rootIN = isRoot(tree, db, inClone, logLsn);
if (rootIN == null) {
/*
* inClone is a root, but no longer in use. Return now, because
* a call to tree.getParentNode will return something
* unexpected since it will try to find a parent.
*/
return null;
} else {
return rootIN;
}
}
/* It's not the root. Can we find it, and if so, is it current? */
inClone.latch(Cleaner.UPDATE_GENERATION);
SearchResult result = null;
try {
result = tree.getParentINForChildIN(
inClone, true /*useTargetLevel*/,
true /*doFetch*/, Cleaner.UPDATE_GENERATION);
if (!result.exactParentFound) {
return null;
}
/* Note that treeLsn may be for a BIN-delta, see below. */
IN parent = result.parent;
long treeLsn = parent.getLsn(result.index);
/*
* The IN in the tree is a never-written IN for a DW db so the IN
* in the file is obsolete. [#15588]
*/
if (treeLsn == DbLsn.NULL_LSN) {
return null;
}
/*
* If tree and log LSNs are equal, then we've found the exact IN we
* read from the log. We know the treeLsn is not for a BIN-delta,
* because it is equal to LSN of the IN (or BIN) we read from the
* log. To avoid a fetch, we can place the inClone in the tree if
* it is not already resident, or use the inClone to mutate the
* delta in the tree to a full BIN.
*/
if (treeLsn == logLsn) {
IN in = parent.loadIN(result.index, Cleaner.UPDATE_GENERATION);
if (in != null) {
in.latch(Cleaner.UPDATE_GENERATION);
if (in.isBINDelta()) {
/*
* The BIN should be dirty here because the most
* recently written logrec for it is a full-version
* logrec. After that logrec was written, the BIN
* was dirtied again, and then mutated to a delta.
* So this delta should still be dirty.
*/
assert(in.getDirty());
/*
* Since we want to clean the inClone full version of
* the bin, we must mutate the cached delta to a full
* BIN so that the next logrec for this BIN can be a
* full-version logrec.
*/
final BIN bin = (BIN) in;
bin.mutateToFullBIN(
(BIN) inClone, false /*leaveFreeSlot*/);
}
} else {
in = inClone;
/*
* Latch before calling postFetchInit and attachNode to
* make those operations atomic. Must use latchNoUpdateLRU
* before the node is attached.
*/
in.latchNoUpdateLRU(db);
in.postFetchInit(db, logLsn);
parent.attachNode(result.index, in, null /*lnSlotKey*/);
}
return in;
}
if (inClone.isUpperIN()) {
/* No need to deal with BIN-deltas. */
return null;
}
/*
* If the tree and log LSNs are unequal, then we must get the full
* version LSN in case the tree LSN is actually for a BIN-delta.
* The only way to do that is to fetch the IN in the tree; however,
* we only need the delta not the full BIN.
*/
final BIN bin =
(BIN) parent.fetchIN(result.index, Cleaner.UPDATE_GENERATION);
treeLsn = bin.getLastFullLsn();
/* Now compare LSNs, since we know treeLsn is the full version. */
final int compareVal = DbLsn.compareTo(treeLsn, logLsn);
/*
* If cmp is > 0 then log entry is obsolete because it is older
* than the version in the tree.
*
* If cmp is < 0 then log entry is also obsolete, because the old
* parent slot was deleted and we're now looking at a completely
* different IN due to the by-level search above.
*/
if (compareVal != 0) {
return null;
}
/*
* Log entry is the full version associated with the BIN-delta
* that's in the tree. To avoid a fetch, we can use the inClone to
* mutate the delta in the tree to a full BIN.
*/
bin.latch(Cleaner.UPDATE_GENERATION);
if (bin.isBINDelta()) {
bin.mutateToFullBIN((BIN) inClone, false /*leaveFreeSlot*/);
}
return bin;
} finally {
if (result != null && result.exactParentFound) {
result.parent.releaseLatch();
}
}
}
/**
* Get the current root in the tree, or null if the inClone is not the
* current root.
*/
private static class RootDoWork implements WithRootLatched {
private final DatabaseImpl db;
private final IN inClone;
private final long logLsn;
RootDoWork(DatabaseImpl db, IN inClone, long logLsn) {
this.db = db;
this.inClone = inClone;
this.logLsn = logLsn;
}
public IN doWork(ChildReference root) {
if (root == null ||
(root.getLsn() == DbLsn.NULL_LSN) || // deferred write root
(((IN) root.fetchTarget(db, null)).getNodeId() !=
inClone.getNodeId())) {
return null;
}
/*
* A root LSN less than the log LSN must be an artifact of when we
* didn't properly propagate the logging of the rootIN up to the
* root ChildReference. We still do this for compatibility with
* old log versions but may be able to remove it in the future.
*/
if (DbLsn.compareTo(root.getLsn(), logLsn) <= 0) {
IN rootIN = (IN) root.fetchTarget(db, null);
rootIN.latch(Cleaner.UPDATE_GENERATION);
return rootIN;
} else {
return null;
}
}
}
/**
* Check if the cloned IN is the same node as the root in tree. Return the
* real root if it is, null otherwise. If non-null is returned, the
* returned IN (the root) is latched -- caller is responsible for
* unlatching it.
*/
private IN isRoot(Tree tree, DatabaseImpl db, IN inClone, long lsn) {
RootDoWork rdw = new RootDoWork(db, inClone, lsn);
return tree.withRootLatchedShared(rdw);
}
/**
* Returns the number of calls to DbTree.getDb during the cleaner run.
*/
int getDbLookupsThisRun() {
return nDbLookupsThisRun;
}
/**
* Reset per-run counters.
*/
private void resetPerRunCounters() {
nINsObsoleteThisRun = 0;
nINsCleanedThisRun = 0;
nINsDeadThisRun = 0;
nINsMigratedThisRun = 0;
nBINDeltasObsoleteThisRun = 0;
nBINDeltasCleanedThisRun = 0;
nBINDeltasDeadThisRun = 0;
nBINDeltasMigratedThisRun = 0;
nLNsObsoleteThisRun = 0;
nLNsExpiredThisRun = 0;
nLNsExtinctThisRun = 0;
nLNsCleanedThisRun = 0;
nLNsDeadThisRun = 0;
nLNsMigratedThisRun = 0;
nLNsMarkedThisRun = 0;
nLNQueueHitsThisRun = 0;
nLNsLockedThisRun = 0;
nDbLookupsThisRun = 0;
nEntriesReadThisRun = 0;
}
/**
* Add per-run counters to total counters.
*/
private void accumulatePerRunCounters() {
cleaner.nINsObsolete.add(nINsObsoleteThisRun);
cleaner.nINsCleaned.add(nINsCleanedThisRun);
cleaner.nINsDead.add(nINsDeadThisRun);
cleaner.nINsMigrated.add(nINsMigratedThisRun);
cleaner.nBINDeltasObsolete.add(nBINDeltasObsoleteThisRun);
cleaner.nBINDeltasCleaned.add(nBINDeltasCleanedThisRun);
cleaner.nBINDeltasDead.add(nBINDeltasDeadThisRun);
cleaner.nBINDeltasMigrated.add(nBINDeltasMigratedThisRun);
cleaner.nLNsObsolete.add(nLNsObsoleteThisRun);
cleaner.nLNsExpired.add(nLNsExpiredThisRun);
cleaner.nLNsExtinct.add(nLNsExtinctThisRun);
cleaner.nLNsCleaned.add(nLNsCleanedThisRun);
cleaner.nLNsDead.add(nLNsDeadThisRun);
cleaner.nLNsMigrated.add(nLNsMigratedThisRun);
cleaner.nLNsMarked.add(nLNsMarkedThisRun);
cleaner.nLNQueueHits.add(nLNQueueHitsThisRun);
cleaner.nLNsLocked.add(nLNsLockedThisRun);
}
/**
* A cache of LNInfo by LSN offset. Used to hold a set of LNs that are
* to be processed. Keeps track of memory used, and when full (over
* budget) the next offset should be queried and removed.
*/
private static class LookAheadCache {
private final SortedMap map;
private final int maxMem;
private int usedMem;
LookAheadCache(int lookAheadCacheSize) {
map = new TreeMap<>();
maxMem = lookAheadCacheSize;
usedMem = MemoryBudget.TREEMAP_OVERHEAD;
}
boolean isEmpty() {
return map.isEmpty();
}
boolean isFull() {
return usedMem >= maxMem;
}
Long nextOffset() {
return map.firstKey();
}
void add(Long lsnOffset, LNInfo info) {
map.put(lsnOffset, info);
usedMem += info.getMemorySize();
usedMem += MemoryBudget.TREEMAP_ENTRY_OVERHEAD;
}
LNInfo remove(Long offset) {
LNInfo info = map.remove(offset);
if (info != null) {
usedMem -= info.getMemorySize();
usedMem -= MemoryBudget.TREEMAP_ENTRY_OVERHEAD;
}
return info;
}
}
}