/*-
* See the file LICENSE for redistribution information.
*
* Copyright (c) 2002-2005
*      Sleepycat Software.  All rights reserved.
*
* $Id: UtilizationSelector.java,v 1.6 2004/12/22 14:11:29 linda Exp $
*/

package com.sleepycat.je.cleaner;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import com.sleepycat.je.DatabaseException;
import com.sleepycat.je.config.EnvironmentParams;
import com.sleepycat.je.dbi.EnvironmentImpl;
import com.sleepycat.je.utilint.DbLsn;

/**
 * Selects files for cleaning based on the utilization profile, and handles
 * retries when cleaning cannot be completed.
 */
class UtilizationSelector {

    /*
     * If we retry a file for maxRetries without success, we'll do normal
     * cleaning for maxRestartRetries and then start retrying again.  But if
     * while in wait-after-retry mode we fail to clean a different file, it
     * becomes the retry file and we discard the former retry file info.
     */

    private UtilizationProfile profile;
    private RetryInfo retryFile;
    private int maxRetries;
    private int maxRestartRetries;
    private int retryCycles;
    private boolean waitAfterRetry;

    UtilizationSelector(EnvironmentImpl env, UtilizationProfile profile)
        throws DatabaseException {

        this.profile = profile;

        maxRetries = env.getConfigManager().getInt
            (EnvironmentParams.CLEANER_RETRIES);
        maxRestartRetries = env.getConfigManager().getInt
            (EnvironmentParams.CLEANER_RESTART_RETRIES);
    }

    /**
     * Returns the file selected for cleaning, or null if none should be
     * cleaned.
     */
    public RetryInfo getFileToClean(Set excludeFiles, boolean forceCleaning)
        throws DatabaseException {

        /* Retry a file that wasn't completed earlier. */
        if (retryFile != null &&
            !waitAfterRetry &&
            !excludeFiles.contains(retryFile.getFileNumber())) {
            return retryFile;
        }

        Set useExcludeFiles = excludeFiles;
        if (retryFile != null) {
            useExcludeFiles = new HashSet(excludeFiles);
            useExcludeFiles.add(retryFile.getFileNumber());
        }

        /* Select a file for cleaning from the profile. */
        Long file = profile.getBestFileForCleaning(useExcludeFiles,
                                                   forceCleaning);
        if (file != null) {
            return new RetryInfo(file);
        } else {
            return null;
        }
    }

    /** 
     * When file processing is complete, setup for retries next time.
     */
    private void onEndFile(RetryInfo file, boolean deleted) {

        if (deleted) {
            /*
             * We sucessfully cleaned a file.
             */
            if (file == retryFile) {
                assert !waitAfterRetry;
                /*
                 * If we cleaned the retry file, forget about it.
                 */
                resetRetryInfo();
            } else if (waitAfterRetry) {
                assert retryFile != null;
                /*
                 * We're in wait-after-retry mode, bump the count.  If we've
                 * exceeded the maxRestartRetries, setup to retry again next
                 * time.
                 */
                retryCycles += 1;
                if (retryCycles >= maxRestartRetries) {
                    waitAfterRetry = false;
                    retryCycles = 0;
                }
            } else {
                assert retryFile == null;
            }
        } else if (retryFile == file) {
            /*
             * We failed to clean the retry file, bump the count.  If we've
             * exeeded maxRetries, setup for wait-after-retry next time.
             */
            retryCycles += 1;
            if (retryCycles >= maxRetries) {
                waitAfterRetry = true;
                retryCycles = 0;
            }
        } else {
            /*
             * We failed to clean a file that is not the retry file.  Retry
             * this file next time and forget about another retry file that
             * failed earlier, if any.
             */
            resetRetryInfo();
            retryFile = file;
        }
    }

    /**
     * Resets retry info when the retry file is successfully cleaned.
     */
    private void resetRetryInfo() {
        retryFile = null;
        retryCycles = 0;
        waitAfterRetry = false;
    }

    /**
     * Cleaner retry information for a log file.
     *
     * <p>When retry information is maintained, a log file is divided into two
     * parts: the processed part and the unprocessed part.</p>
     *
     * <p>The processed part always starts at the beginning of the file and has
     * been fully cleaned except for pending entries.  Those entries are
     * returned by the pendingLsnIterator() method, and should be retried when
     * the file is processed again.</p>
     *
     * <p>The unprocessed part follows the processed part and starts with the
     * LSN returned by the getFirstUnprocessedLsn() method.  All entries in the
     * log from that LSN onward, including INs, should be cleaned when the file
     * is processed.  The unprocessed part of the file is present only if the
     * isFileFullyProcessed() method returns false.</p>
     *
     * <p>When there are no more pending entries in the processed part of the
     * file and the file is fully processed, the canFileBeDeleted() method will
     * return true and the file should be deleted.</p>
     */
    class RetryInfo {

        private Long fileNum;
        private long firstUnprocessedLsn = DbLsn.NULL_LSN;
        private List pendingLsns;
        private boolean fullyProcessed;

        RetryInfo(Long fileNum) {
            this.fileNum = fileNum;
            pendingLsns = new ArrayList();
        }

        /**
         * Returns the log file number.
         */
        public Long getFileNumber() {
            return fileNum;
        }

        /**
         * Ends processing of one cleaner cycle and indicates whether the file
         * was actually deleted by the cleaner.
         */
        public void endProcessing(boolean deleted) {

            onEndFile(this, deleted);
        }

        /**
         * Returns whether all LSNs are obsolete.
         */
        public boolean canFileBeDeleted() {

            if (fullyProcessed && pendingLsns.isEmpty()) {
                return true;
            } else {
                return false;
            }
        }

        /**
         * Specifies that the file has been fully processed and only pending
         * LNs may prevent the file from being deleted.
         */
        public void setFileFullyProcessed() {

            fullyProcessed = true;
        }

        /**
         * Returns true if all entries were previously processed, or false if
         * more entries should be processed starting with
         * getFirstUnprocessedLsn().
         */
        public boolean isFileFullyProcessed() {

            return fullyProcessed;
        }

        /**
         * Specifies that all LSNs from the given LSN have not been processed.
         */
        public void setFirstUnprocessedLsn(long lsn) {

            assert DbLsn.getFileNumber(lsn) == fileNum.longValue();
            firstUnprocessedLsn = lsn;
        }

        /**
         * When isFileFullyProcessed() is false, returns the first LSN for
         * processing the file or null to start at the beginning of the file.
         */
        public long getFirstUnprocessedLsn() {

            return firstUnprocessedLsn;
        }

        /**
         * Returns an array of LSNs for LNs that could not be migrated in a
         * prior cleaning attempt, or null if no LNs are pending.
         */
        public long[] getPendingLsns() {

	    int n = pendingLsns.size();
            if (n > 0) {
                Long[] lsns = new Long[n];
                pendingLsns.toArray(lsns);
		long[] ret = new long[n];
		for (int i = 0; i < n; i++) {
		    ret[i] = lsns[i].longValue();
		}
                return ret;
            } else {
                return null;
            }
        }

        /**
         * Returns whether the given LN is known to be obsolete.
         */
        public boolean isObsoleteLN(long lsn, long nodeId) {

            assert DbLsn.getFileNumber(lsn) == fileNum.longValue();
            return false; // We don't currently do this optimization.
        }

        /**
         * Changes the status of a given LN to obsolete.
         */
        public void setObsoleteLN(long lsn, long nodeId) {

            assert DbLsn.getFileNumber(lsn) == fileNum.longValue();
            pendingLsns.remove(new Long(lsn));
        }

        /**
         * Changes the status of a given LN to pending.
         */
        public void setPendingLN(long lsn, long nodeId) {

            assert DbLsn.getFileNumber(lsn) == fileNum.longValue();
            pendingLsns.add(new Long(lsn));
        }
    }
}
