All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sleepycat.je.cleaner.ExpirationProfile Maven / Gradle / Ivy

The newest version!
/*-
 * 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 java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.sleepycat.bind.tuple.SortedPackedLongBinding;
import com.sleepycat.je.Cursor;
import com.sleepycat.je.DatabaseEntry;
import com.sleepycat.je.DbInternal;
import com.sleepycat.je.Get;
import com.sleepycat.je.LockMode;
import com.sleepycat.je.ProgressListener;
import com.sleepycat.je.Put;
import com.sleepycat.je.RecoveryProgress;
import com.sleepycat.je.dbi.DatabaseImpl;
import com.sleepycat.je.dbi.DbType;
import com.sleepycat.je.dbi.EnvironmentImpl;
import com.sleepycat.je.dbi.StartupTracker;
import com.sleepycat.je.dbi.TTL;
import com.sleepycat.je.txn.BasicLocker;
import com.sleepycat.je.txn.Locker;
import com.sleepycat.je.utilint.LoggerUtils;
import com.sleepycat.je.utilint.Pair;

/**
 * A cache of the histograms for all files, except for the last file. Also
 * caches the number of bytes expired in the current interval.
 *
 * No memory budgeting is performed because the size of these data structures
 * is small compared to the Btree they represent. The serialized form of the
 * histogram is cached, which is a small number of bytes per file. If no data
 * in a file expires, it will not have a cache entry.
 *
 * Possible future optimization: If there is contention on this data structure,
 * the refresh method could create a read-only map containing the current number
 * of expired bytes, for access by getExpiredBytes without synchronization.
 */
public class ExpirationProfile {

    private static Pair PAIR_OF_ZEROS = new Pair<>(0, 0);

    private final EnvironmentImpl env;

    /*
     * The 'map' of file number to Histogram, protected by its own mutex.
     *
     * Note that if the map and completedTrackers mutexes are both held, they
     * must be acquired in that order.
     */
    private final Map map;

    /*
     * The the expiration times in days and hours of last refresh, i.e, the
     * cached bytes in each Histogram are those that were expired on this
     * day/hour. Both fields are protected by the 'map' mutex.
     */
    private int lastRefreshHour = -1;
    private int lastRefreshDay = -1;

    /*
     * Whether any expiration times are in hours. If false, all intervals are
     * in days. Protected by the 'map' mutex.
     */
    private boolean anyExpirationInHours;

    /*
     * Map of file number to completed tracker. Protected by its own mutex.
     *
     * Note that if the map and completedTrackers mutexes are both held, they
     * must be acquired in that order.
     */
    private final Map completedTrackers;

    /*
     * The expiration summary DB. Its key is the file number, a long. Its data
     * is the serialized form of the histogram, or is empty (zero length) if
     * the file has no expired data. The latter case includes files created
     * before the TTL feature was added.
     */
    private DatabaseImpl db;

    public ExpirationProfile(final EnvironmentImpl env) {
        this.env = env;
        map = new HashMap<>();
        completedTrackers = new HashMap<>();
    }

    /** Make a copy for use in utilities, etc. */
    public ExpirationProfile(final ExpirationProfile other) {
        env = other.env;
        db = other.db;
        synchronized (other.map) {
            map = new HashMap<>(other.map);
        }
        completedTrackers = Collections.emptyMap();
    }

    /**
     * Called at the end of recovery to open the expiration DB, and cache its
     * records in the profile's map.
     *
     * Also collects expiration info for any complete file having expiration
     * info that was not written to the DB earlier, due to a crash for example.
     *
     * Also initializes the tracker for the current file in the log manager,
     * reading/tracking the existing entries in that file.
     */
    public void populateCache(
        final StartupTracker.Counter counter,
        final ProgressListener listener) {

        synchronized (map) {

            assert db == null;
            assert completedTrackers.isEmpty();

            db = env.getDbTree().openNonRepInternalDB(DbType.EXPIRATION);

            if (db == null) {
                /* Read-only env with no expiration summary DB. */
                return;
            }

            final DatabaseEntry key = new DatabaseEntry();
            final DatabaseEntry data = new DatabaseEntry();

            /* Ordered array of file numbers. */
            final Long[] existingFiles =
                env.getFileManager().getAllFileNumbers();

            /* Parallel array to existingFiles. */
            final boolean[] filesHaveRecords =
                new boolean[existingFiles.length];

            /*
             * For the last file we must always get its expiration info and
             * then initialize the log manager's tracker. Note that its DB
             * record, if any, is deleted below.
             */
            final FileProcessor processor = env.getCleaner().createProcessor();
            final long lastFileNum = env.getFileManager().getCurrentFileNum();

            if (!env.isReadOnly()) {
                if (existingFiles.length > 0) {

                    /* Flush to ensure the cleaner can read all entries. */
                    env.flushLog(false /*fsync*/);

                    final ExpirationTracker tracker =
                        processor.countExpiration(lastFileNum);

                    env.getLogManager().initExpirationTracker(tracker);
                } else {
                    env.getLogManager().initExpirationTracker(
                        new ExpirationTracker(0));
                }
            }

            /*
             * Populate map with existing records in the DB that correspond to
             * existing files. Delete records in the DB that do not correspond
             * to existing files, to clean-up past errors. Also delete the
             * record for the last file, if it exists.
             */
            final Locker locker = BasicLocker.createBasicLocker(
                env, false /*noWait*/);

            try (final Cursor cursor =
                 DbInternal.makeCursor(db, locker, null)) {

                while (cursor.get(key, data, Get.NEXT, null) != null) {

                    counter.incNumRead();

                    final long fileNum =
                        SortedPackedLongBinding.entryToLong(key);

                    final int i = Arrays.binarySearch(existingFiles, fileNum);

                    if (i >= 0 && existingFiles[i] < lastFileNum) {

                        filesHaveRecords[i] = true;

                        final byte[] serializedForm = data.getData();

                        if (serializedForm.length > 0) {
                            counter.incNumProcessed();
                            map.put(
                                fileNum,
                                new ExpInfo(serializedForm, 0));
                        }
                    } else if (!env.isReadOnly()) {
                        counter.incNumDeleted();
                        cursor.delete();
                    }
                }
            } finally {
                locker.operationEnd();
            }

            /*
             * If a record is missing for an existing file, use the cleaner to
             * get the expiration info for the file, and then add a record to
             * the DB and to the map. Note that the last file is not processed.
             */
            for (int i = 0;
                 i < existingFiles.length && existingFiles[i] < lastFileNum;
                 i += 1) {

                if (filesHaveRecords[i]) {
                    continue;
                }

                final long fileNum = existingFiles[i];

                counter.incNumAux();

                if (listener != null) {
                    listener.progress(
                        RecoveryProgress.POPULATE_EXPIRATION_PROFILE,
                        1, -1);
                }

                final ExpirationTracker tracker =
                    processor.countExpiration(fileNum);

                putFile(tracker, 0);

                LoggerUtils.info(
                    env.getLogger(), env,
                    "Loaded missing expiration data from file 0x" +
                        Long.toHexString(fileNum));
            }
        }
    }

    /**
     * Writes a record in the expiration summary DB for the given tracker,
     * and (if there is any data with an expiration time) adds it to the map.
     *
     * Because this method and {@link #removeFile} perform Btree operations
     * while synchronized on the 'map', it is important that an IN is not
     * latched when calling these methods, which could cause a deadlock. Also,
     * an IN latch should not be held while calling a method that synchronizes
     * on {@link FileSelector}, since methods in this class are called while
     * synchronized on FileSelector, and this could cause a 3-way deadlock
     * [#25613].
     */
    void putFile(final ExpirationTracker tracker, final int expiredSize) {

        final long fileNum = tracker.getFileNum();
        final byte[] serializedForm = tracker.serialize();

        synchronized (map) {

            if (db != null && !env.isReadOnly()) {
                final DatabaseEntry key = new DatabaseEntry();
                final DatabaseEntry data = new DatabaseEntry();

                SortedPackedLongBinding.longToEntry(fileNum, key);

                data.setData(serializedForm);

                final Locker locker = BasicLocker.createBasicLocker(
                    env, false /*noWait*/);

                try (final Cursor cursor =
                         DbInternal.makeCursor(db, locker, null)) {

                    cursor.put(key, data, Put.OVERWRITE, null);

                } finally {
                    locker.operationEnd();
                }
            }

            if (serializedForm.length > 0) {
                map.put(
                    fileNum,
                    new ExpInfo(serializedForm, expiredSize));
            }
        }
    }

    /**
     * Remove entry for a file from the map and DB, when the file is deleted.
     */
    void removeFile(final long fileNum) {

        if (db == null || env.isReadOnly()) {
            return;
        }

        synchronized (map) {

            map.remove(fileNum);

            final DatabaseEntry key = new DatabaseEntry();
            SortedPackedLongBinding.longToEntry(fileNum, key);

            final Locker locker = BasicLocker.createBasicLocker(
                env, false /*noWait*/);

            try (final Cursor cursor =
                 DbInternal.makeCursor(db, locker, null)) {

                if (cursor.get(
                    key, null, Get.SEARCH,
                    LockMode.RMW.toReadOptions()) != null) {

                    cursor.delete(null);
                }
            } finally {
                locker.operationEnd();
            }
        }
    }

    /**
     * Called after a file flip. The tracker is completed in the sense that the
     * file is completely written, but there may be pending calls to
     * {@link ExpirationTracker#track(int, boolean, int)} for some writing
     * threads. This is because track is not called under the LWL.
     */
    public void addCompletedTracker(final ExpirationTracker tracker) {

        if (db == null) {
            return;
        }

        final long fileNum = tracker.getFileNum();

        synchronized (completedTrackers) {
            assert !completedTrackers.containsKey(fileNum);

            completedTrackers.put(fileNum, tracker);
        }
    }

    /**
     * Periodically, and when refreshing the profile, we process completed
     * trackers that were added to the completedTrackers queue at the time of a
     * file flip. If truly complete, they are added to the profile DB and map.
     * 

* Although this method synchronizes on completedTrackers, which is in the * main write path, it does very little work while synchronized and should * not impact user operations. */ void processCompletedTrackers() { /* Only one thread at a time should process them. */ synchronized (map) { /* * Make a copy in order to process them without holding their * mutex, to avoid blocking addCompletedTracker, which is in the * main write path. */ final List trackers; synchronized (completedTrackers) { trackers = completedTrackers.isEmpty() ? Collections.emptyList() : new ArrayList<>(completedTrackers.values()); } for (final ExpirationTracker tracker : trackers) { if (tracker.hasPendingTrackCalls()) { /* Not quite completed. */ continue; } putFile(tracker, 0); synchronized (completedTrackers) { completedTrackers.remove(tracker.getFileNum()); } } } } /** * Updates the expired bytes in the expiration profile according to the * data that has expired at the given time. The recalculation is only * actually performed once per hour, on hour boundaries, since data * expires on (at most) hour boundaries. *

* Also calls processCompletedTrackers to process any completed trackers * by adding them to the DB and to the histogram map. *

* Should be called periodically (more than once per file written), and * before calling {@link #getExpiredBytes}. */ public void refresh(final long time) { if (db == null) { return; } /* Synchronize to protect map and the lastRefreshXxx fields. */ synchronized (map) { /* * Leverage that fact that this method is called periodically * (every cleaner wakeup) to ensure that we process complete * trackers at least once per file written. This is done even if * we do not need to recalculate the expiration profile, i.e., * more than once per hour. */ processCompletedTrackers(); /* * Get last hour boundary, rounding down to the closest hour. If * an hour has not passed (and this is not the first time called), * then return and expect that we'll try again later. */ final int hourLimit = (int) (time / TTL.MILLIS_PER_HOUR); if (hourLimit == lastRefreshHour) { return; } final int dayLimit = hourLimit / 24; final boolean newDayLimit = (dayLimit != lastRefreshDay); lastRefreshHour = hourLimit; lastRefreshDay = dayLimit; anyExpirationInHours = false; for (final ExpInfo info : map.values()) { if (ExpirationTracker.isExpirationInHours( info.serializedForm)) { anyExpirationInHours = true; break; } } /* * If all expiration times are on day boundaries, and we have not * started a new day, there is nothing more to do. */ if (!newDayLimit && !anyExpirationInHours) { return; } /* * Recalculate expired bytes for the current day/hour, saving the * previous value. */ for (final ExpInfo info : map.values()) { info.previousExpiredBytes = info.currentExpiredBytes; info.currentExpiredBytes = ExpirationTracker.getExpiredBytes( info.serializedForm, dayLimit, hourLimit); } } } /** * Returns the number of expired bytes for the given file. Uses the value * calculated by the last call to {@link #refresh}. */ public int getExpiredBytes(final long fileNum) { synchronized (map) { final ExpInfo info = map.get(fileNum); return (info != null) ? info.currentExpiredBytes : 0; } } /** * Returns the number of expired bytes for the given file. Two values are * returned: the total expired at the given time, and a potentially smaller * amount that gradually expires over the current hour or day interval. * Uses the values calculated by the last call to {@link #refresh}. * * The amount that gradually expires is the amount that expires in the * current time interval, which is one day if all data in the profile * expires on day boundaries, and otherwise is one hour. If this is the * first interval for the file (after a restart or a revisal run), all * expired bytes are considered expired in the current interval and expire * gradually. * * @return pair of {allExpiredBytes, gradualExpiredBytes}. */ public Pair getExpiredBytes( final long fileNum, final long time) { synchronized (map) { final ExpInfo info = map.get(fileNum); if (info == null) { return PAIR_OF_ZEROS; } final int newlyExpiredBytes = info.currentExpiredBytes - info.previousExpiredBytes; if (newlyExpiredBytes == 0) { return new Pair<>( info.currentExpiredBytes, info.currentExpiredBytes); } final long intervalMs = anyExpirationInHours ? TTL.MILLIS_PER_HOUR : TTL.MILLIS_PER_DAY; final long currentMs = time % intervalMs; final int gradualBytes = info.previousExpiredBytes + ((int) ((newlyExpiredBytes * currentMs) / intervalMs)); return new Pair<>(info.currentExpiredBytes, gradualBytes); } } public String toString(final long fileNum) { synchronized (map) { final ExpInfo info = map.get(fileNum); return (info != null) ? info.toString() : "NoExpInfo"; } } /** * Contains cached information about expiration for a data file. */ private static class ExpInfo { /** * Cached serialized form, use to recompute the current expired bytes * for a given expiration time. */ final byte[] serializedForm; /** * The number of expired bytes for the given file. This is the value * calculated by the last call to {@link ExpirationProfile#refresh}. */ int currentExpiredBytes = 0; /** * The number of bytes that expired prior to the current interval. * Calculated by the previous refresh for which the interval changed. */ int previousExpiredBytes = 0; ExpInfo(final byte[] serializedForm, final int currentExpiredBytes) { this.serializedForm = serializedForm; this.currentExpiredBytes = currentExpiredBytes; } @Override public String toString() { return "{ExpInfo currentBytes = " + currentExpiredBytes + " " + ExpirationTracker.toString(serializedForm) + '}'; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy