Package com.sleepycat.je.sync.impl

Source Code of com.sleepycat.je.sync.impl.LogChangeReader$LogChangeIterator

package com.sleepycat.je.sync.impl;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;

import com.sleepycat.je.DatabaseEntry;
import com.sleepycat.je.DbInternal;
import com.sleepycat.je.Environment;
import com.sleepycat.je.EnvironmentFailureException;
import com.sleepycat.je.Transaction;
import com.sleepycat.je.config.EnvironmentParams;
import com.sleepycat.je.dbi.DatabaseId;
import com.sleepycat.je.dbi.DatabaseImpl;
import com.sleepycat.je.dbi.EnvironmentImpl;
import com.sleepycat.je.log.LNFileReader;
import com.sleepycat.je.log.LogEntryType;
import com.sleepycat.je.log.entry.LNLogEntry;
import com.sleepycat.je.sync.ChangeReader;
import com.sleepycat.je.sync.SyncDataSet;
import com.sleepycat.je.sync.SyncDatabase;
import com.sleepycat.je.sync.SyncProcessor;
import com.sleepycat.je.sync.impl.LogChangeSet.LogChangeSetBinding;
import com.sleepycat.je.txn.Locker;
import com.sleepycat.je.txn.LockerFactory;
import com.sleepycat.je.utilint.DbLsn;
import com.sleepycat.je.utilint.TestHook;
import com.sleepycat.je.utilint.TestHookExecute;
import com.sleepycat.je.utilint.VLSN;

/*
* LogChangeReader implements its own LogChangeIterator, which uses a
* LNFileReader to scan the log to find out the log entries we're interested.
*
* To track the next/commit point on the log, the JEChangeTxn has to remembered
* the start and commit (LSN for a standalone Environment, VLSN for a
* replicated Environment) point, and all JEChangeTxn that created during the
* log scan are saved in LogChangeReader.txns.
*
* When the LogChangeReader is first created, we have to look for the VLSN->LSN
* mapping to find out the LogChangeReader.readStart (the read start position
* of the LNFileReader) if it's a replicated Environment. And we'll use the
* readStart to keep track of the reading start position of LNFileReader, so
* that we don't need to look for the VLSN->LSN mapping when a new LNFileReader
* is created next time (former LNFileReader meets the end of the log, have to
* create a new LNFileReader).
*
* Also, LogChangeReader uses the lastSyncEnd to keep track of the commit point
* of the latest committed transaction, and we set it as
* LogChangeSet.lastSyncEnd when the discardChanges is invoked.
*
* Note that LogChangeReader will only read the entries between the
* nextSyncStart and GlobalCBVLSN (it's durable now) to make sure hard recovery
* wouldn't cause the server to roll back committed transactions. Also, the
* txns is a LinkedHashMap, so the first JEChangeTxn in that map while
* discardChanges is invoked is the earliest uncommitted transaction, and its
* startPoint should be recorded as the LogChangeSet.nextSyncStart.
*
* The special case for the LogChangeReader is when it encounters a commit
* entry, it'll not only set the readStart as the commit entry's lsn, but also
* add the entry size of the commit log entry, so that the reader won't read
* the commit entry twice, to avoid treating a single transaction commit entry
* as a whole transaction.
*
* TODO list:
* 1. Implement the consolidation.
* 2. Currently make the SyncProcessor.readChangeSetData and
*    SyncProcessor.writeChangeSetData public, need to make it protected.
* 3. May change the LNFileReader's reading end position from
*    DbLsn.NULL_LSN (the log end) to some value to reduce the read size.
* 4. Buffering of large transactions is an issue, since it could cause OOME.
*    If a buffered transaction exceeds a configured size, we could discard all
*    but the LSNs from the buffer and read the entries from the log after we
*    encounter the commit.
* 5. Figure out a way to throw out an exception if the user doesn't work in
*    correct sequence.
* 6. Adding the support for non-transactional LN log entries.
*/
public class LogChangeReader implements ChangeReader {

    /* The target entry types for the reader. */
    public static final LogEntryType[] targetTypes = new LogEntryType[] {
        LogEntryType.LOG_INS_LN_TRANSACTIONAL,     // insert entry type
        LogEntryType.LOG_UPD_LN_TRANSACTIONAL,     // update entry type
        LogEntryType.LOG_DEL_LN_TRANSACTIONAL,     // delete entry type
        LogEntryType.LOG_DEL_DUPLN_TRANSACTIONAL,  // dup delete entry type
        LogEntryType.LOG_TXN_COMMIT,               // txn commit
        LogEntryType.LOG_TXN_ABORT                 // txn abort
    };

    /* A binding used to serialize and deserialize the LogChangeSet. */
    private static final LogChangeSetBinding binding =
        new LogChangeSetBinding();

    private final EnvironmentImpl envImpl;
    private final String dataSetName;
    private final SyncProcessor processor;
    private final boolean consolidateTransactions;
    private final long consolidateMaxMemory;

    /* Used to keep track of next/commit point of changed transactions. */
    private LogChangeSet changeSet;

    /* The reading start position of the LNFileReader. */
    private long readStart;

    /*
     * LogChangeReader has to read the VLSN->LSN mapping if it's the first time
     * to create the LNFileReader, then it will use the readStart to keep track
     * of the next reading start position.
     */
    private boolean firstCreateReader = true;

    /* Save the last transaction commit point. */
    private long lastSyncEnd = LogChangeSet.NULL_POSITION;

    /* Keep track of the ChangeTxns read from JE log. */
    private final Map<Long, ChangeTxn> txns =
        new LinkedHashMap<Long, ChangeTxn>();

    /* Save the database id and info for synced databases. */
    private final Map<DatabaseId, DbInfo> syncDbs =
        new HashMap<DatabaseId, DbInfo>();

    /* TestHook used for concurrent unit tests. */
    private TestHook waitHook;

    public LogChangeReader(Environment env,
                           String dataSetName,
                           SyncProcessor processor,
                           boolean consolidateTransactions,
                           long consolidateMaxMemory) {
        this.envImpl = DbInternal.getEnvironmentImpl(env);
        this.dataSetName = dataSetName;
        this.processor = processor;
        this.consolidateTransactions = consolidateTransactions;
        this.consolidateMaxMemory = consolidateMaxMemory;

        initChangeSet(env);

        getSyncDbs(env);

        readStart = changeSet.getNextSyncStart();

        assert readStart != LogChangeSet.NULL_POSITION;
    }

    /*
     * Initiate the LogChangeSet, if there exists any exceptions while reading
     * the ChangeSet metadata from SyncDB, invalidate the Environment.
     */
    private void initChangeSet(Environment env) {
        Transaction txn = env.beginTransaction(null, null);
        boolean success = false;

        try {
            DatabaseEntry changeSetData = new DatabaseEntry();

            processor.readChangeSetData(txn, dataSetName, changeSetData);

            /*
             * If there is no ChangeSet information for this SyncDataSet in
             * SyncDB, create a new one, otherwise deserialize the
             * DatabaseEntry.
             */
            changeSet = (changeSetData.getData() == null) ?
                new LogChangeSet() : binding.entryToObject(changeSetData);
            success = true;
        } finally {
           if (success) {
              txn.commit();
           } else {
              txn.abort();
           }
        }
    }

    /*
     * Get database ID and name information for sync'ed databases in this
     * SyncDataSet.
     */
    private void getSyncDbs(Environment env) {
        /* Get the DatabaseId for all synced databases. */
        SyncDataSet dataSet = processor.getDataSets().get(dataSetName);
        Iterator<SyncDatabase> databases = dataSet.getDatabases().iterator();

        while (databases.hasNext()) {
            Locker readLocker = null;
            boolean operationOK = false;
            DatabaseImpl dbImpl = null;
            String dbName = databases.next().getLocalName();

            try {
                readLocker = LockerFactory.getReadableLocker
                    (env, null,
                     false, /* transactional */
                     false /* readCommittedIsolation */);

                dbImpl = envImpl.getDbTree().getDb(readLocker, dbName, null);
                if (dbImpl != null) {
                    syncDbs.put(dbImpl.getId(),
                                new DbInfo(dbName,
                                           dbImpl.getSortedDuplicates()));
                }
                operationOK = true;
            } finally {
                if (dbImpl != null) {
                    envImpl.getDbTree().releaseDb(dbImpl);
                }

                if (readLocker != null) {
                    readLocker.operationEnd(operationOK);
                }
            }
        }
    }

    public class DbInfo {
        public final String name;
        public final boolean duplicates;

        DbInfo(final String name, final boolean duplicates) {
            this.name = name;
            this.duplicates = duplicates;
        }
    }

    public Map<DatabaseId, DbInfo> getSyncDbs() {
        return syncDbs;
    }

    public LogChangeSet getChangeSet() {
        return changeSet;
    }

    /*
     * Create an LNFileReader to read the log entries.
     *
     * TODO: Keep track of last fsynced LSN in the FileManager, so we can
     * export entries in the last file if an fsync has occurred, also consider
     * updating the GlobalDurableVLSN to the last fsynced VLSN.
     *
     * We're currently reading entries until the second largest log file, but
     * if the log file size is fairly large, it is not efficient to do so.
     * Because there will be some checkpoints in the last log file or a
     * transaction commit with WRITE_SYNC would flush the entries to the disk
     * and make them fsynced. We may need to find a way to keep track of the
     * last fsynced LSN to improve the performance.
     */
    private LNFileReader createFileReader() {
        final int readBufferSize = envImpl.getConfigManager().getInt
            (EnvironmentParams.LOG_ITERATOR_READ_SIZE);
       
        /*
         * We shouldn't read unfsynced entries for a standalone Environment.
         * The last entry on the second largest log file must be fsynced, but
         * we won't use its concrete LSN since it needs to read the whole file.
         *
         * To avoid this, we can use a fake lsn that represents the largest LSN
         * on the second largest log file.
         */
        final long currentFileNum =
            envImpl.getFileManager().getCurrentFileNum();
        long finishLsn =
            DbLsn.makeLsn(currentFileNum - 1, DbLsn.MAX_FILE_OFFSET);

        try {

            /*
             * To avoid hard recovery, only read log entries between the read
             * start and GlobalDurablVLSN for a ReplicatedEnvironment.
             */
            if (envImpl.isReplicated()) {
                VLSN durableVLSN = envImpl.getGroupDurableVLSN();

                /* Read nothing if GlobalCBVLSN is null. */
                if (durableVLSN.isNull()) {
                    return null;
                }

                /*
                 * Read nothing if read start is greater than GroupDurableVLSN.
                 */
                if (durableVLSN.compareTo(new VLSN(readStart)) <= 0) {
                    return null;
                }

                if (firstCreateReader) {
               
                    /*
                     * Find the LSN for readStart (it's a VLSN now) when
                     * LNFileReader is first created.
                     */
                    readStart = envImpl.getLsnForVLSN(new VLSN(readStart),
                                                      readBufferSize);
                    firstCreateReader = false;
                }
       
                /* Find LSN for the durableVLSN. */
                finishLsn =
                    envImpl.getLsnForVLSN(durableVLSN, readBufferSize);
            }
        } catch (EnvironmentFailureException e) {
            e.addErrorMessage("SyncDataSet: " + dataSetName +
                              ", SyncProcessor: " + processor.getName());
            throw e;
        }

        LNFileReader reader = new LNFileReader(envImpl,
                                               readBufferSize,
                                               readStart,
                                               true,
                                               DbLsn.NULL_LSN,
                                               finishLsn,
                                               null,
                                               DbLsn.NULL_LSN);
       
        for (LogEntryType entryType : targetTypes) {
            reader.addTargetType(entryType);
        }

        return reader;
    }

    public class JEChange implements Change {
        private final ChangeType type;
        private final DatabaseEntry key;
        private final DatabaseEntry data;
        private final String dbName;

        public JEChange(ChangeType type,
                        DatabaseEntry key,
                        DatabaseEntry data,
                        String dbName) {
            this.type = type;
            this.key = key;
            this.data = data;
            this.dbName = dbName;
        }

        public ChangeType getType() {
            return type;
        }

        public DatabaseEntry getKey() {
            return key;
        }

        public DatabaseEntry getData() {
            return data;
        }

        public String getDatabaseName() {
            return dbName;
        }
    }

    public class JEChangeTxn implements ChangeTxn {
        private final long txnId;
        /* Start point of this transaction, either VLSN or LSN. */
        private long startPoint = LogChangeSet.NULL_POSITION;
        /* Commit point of this transaction, either VLSN or LSN. */
        private long commitPoint = LogChangeSet.NULL_POSITION;

        private final Set<String> dbNames = new HashSet<String>();
        /* The changes should be keep in sequence. */
        private final ArrayList<Change> operations = new ArrayList<Change>();

        public JEChangeTxn(long txnId, long startPoint) {
            this.txnId = txnId;
            this.startPoint = startPoint;
        }

        public long getTransactionId() {
            return txnId;
        }

        public String getDataSetName() {
            return dataSetName;
        }

        public Set<String> getDatabaseNames() {
            return dbNames;
        }

        public Iterator<Change> getOperations() {
            return operations.iterator();
        }

        /* Write the ChangeSet information to SyncDB for JDBCProcessor. */
        public void discardChanges(Transaction txn) {
            synchronized (envImpl.getSyncCleanerBarrier()) {
                /* Free the memory held by this ChangeTxn. */
                clear();

                /*
                 * Set the LogChangeSet.lastSyncEnd to the commit point of this
                 * ChangeTxn.
                 */
                changeSet.setLastSyncEnd(commitPoint);

                /*
                 * Reset the LogChangeSet.nextSyncStart and write the
                 * LogChangeSet into SyncDB.
                 */
                resetChangeSetNextSyncStart();
                writeSyncDB(txn);
            }
        }

        /* Add a change for this transaction. */
        private void addChange(String dbName, Change operation) {
            if (!dbNames.contains(dbName)) {
                dbNames.add(dbName);
            }
            operations.add(operation);
        }

        private void setCommitPoint(long commitPoint) {
            this.commitPoint = commitPoint;
        }

        private long getStartPoint() {
            return startPoint;
        }

        private long getCommitPoint() {
            return commitPoint;
        }

        /* Clear the Changes hold by the ChangeTxn. */
        private void clear() {
            dbNames.clear();
            operations.clear();
        }
    }

    class LogChangeIterator implements Iterator<ChangeTxn> {
        private LNFileReader reader;
        private ChangeTxn nextChangeTxn;

        public LogChangeIterator() {
            reader = createFileReader();
        }

        public boolean hasNext() {

            /*
             * TODO: need to figure out a way to deal with the invalid
             * invocation that users try to call LogChangeIterator.next()
             * after they remove the SyncDataSet from the SyncProcessor.
             */
            if (reader == null) {
                return false;
            }

            if (nextChangeTxn != null) {
                return true;
            }

            if (!hasNextChangeTxn()) {
                nextChangeTxn = null;
                return false;
            }

            return true;
        }

        /*
         * We can remove the ChangeTxn from txns, but we can't remove the
         * changes saved in nextChangeTxn, otherwise users get no changes.
         * Should we remind users it's responsibility to remove those changes
         * from the memory?
         */
        public ChangeTxn next() {
            if (!hasNext()) {
                throw new NoSuchElementException
                    ("No ChangeTxn can be read from the log.");
            }

            /* Remove the ChangeTxn from the list. */
            ChangeTxn returnVal = nextChangeTxn;
            txns.remove(nextChangeTxn.getTransactionId());
            nextChangeTxn = null;

            /* Set lastSyncEnd remember the latest commit position. */
            long commitPoint = ((JEChangeTxn) returnVal).getCommitPoint();
            if (lastSyncEnd < commitPoint) {
                lastSyncEnd = commitPoint;
            }

            return returnVal;
        }

        public void remove() {
            throw new UnsupportedOperationException
                ("Iterator returned by ChangeReader.getChangeTxns() doesn't " +
                 "support remove operation.");
        }

        /* True if we reads a new ChangeTxn. */
        private boolean hasNextChangeTxn() {
            while (reader.readNextEntry()) {
                /* Deal with a transactional target LN log entry. */
                if (reader.isLN() && !reader.isInvisible()) {
                    addChange();
                }

                /* Deal with a commit log entry. */
                if (reader.isCommit()) {
                    if (isValidChangeTxn()) {
                        nextChangeTxn = txns.get(reader.getTxnCommitId());
                        return true;
                    }
                }

                /*
                 * Release the memory held by an Abort transaction and remove
                 * it from the ChangeTxns list.
                 */
                if (reader.isAbort()) {
                    JEChangeTxn txn =
                        (JEChangeTxn) txns.get(reader.getTxnAbortId());

                    /*
                     * Only do this when the transaction belongs to this
                     * SyncDataSet.
                     */
                    if (txn != null) {
                        resetReadStart(false);
                        txn.clear();
                        txns.remove(txn.getTransactionId());
                    }
                }
            }

            return false;
        }

        /* Add a data change. */
        private void addChange() {
            DatabaseId dbId = reader.getDatabaseId();

            /* Do nothing if this entry doesn't belong to this SyncDataSet. */
            if (!syncDbs.containsKey(dbId)) {
                return;
            }

            DbInfo dbInfo = syncDbs.get(dbId);
            long txnId = reader.getTxnId();

            /* Create a ChangeTxn if it doesn't exist. */
            JEChangeTxn txn = (JEChangeTxn) txns.get(txnId);
            if (txn == null) {
                txn = new JEChangeTxn(txnId, getEntryPoint());
                txns.put(txnId, txn);
            }

            /*
             * Get the key and data field of this LN. We want the 'user' key
             * and data, not the raw key and data, because we're replaying the
             * user-level operation.  Duplicate DBs have two-part keys
             * containing the key and data, and LNLogEntry.getUserKeyData is
             * used to convert this back to the user operation params.
             */
            LNLogEntry lnEntry = reader.getLNLogEntry();
            lnEntry.postFetchInit(dbInfo.duplicates);
            DatabaseEntry key = new DatabaseEntry();
            DatabaseEntry data = (lnEntry.getLN().getData() != null) ?
                (new DatabaseEntry()) :
                null;
            lnEntry.getUserKeyData(key, data);

            /* Make sure the entry type is valid. */
            LogEntryType entryType =
                LogEntryType.findType(reader.getLogEntryType());
            assert entryType.isUserLNType() && entryType.isTransactional();

            /* Figure out the correct change type. */
            ChangeType type = ChangeType.DELETE;
            if (entryType == LogEntryType.LOG_INS_LN_TRANSACTIONAL) {
                type = ChangeType.INSERT;
            } else if (entryType == LogEntryType.LOG_UPD_LN_TRANSACTIONAL) {
                type = ChangeType.UPDATE;
            }

            txn.addChange(dbInfo.name,
                          new JEChange(type, key, data, dbInfo.name));
            resetReadStart(false);
        }

        /* Check a commit entry is an valid commit. */
        private boolean isValidChangeTxn() {
            JEChangeTxn txn = (JEChangeTxn) txns.get(reader.getTxnCommitId());

            /*
             * Do nothing when encounters a transaction commit that doesn't
             * belong to this SyncDataSet. Note: JE doesn't log a commit entry
             * if the transaction does nothing.
             */
            if (txn == null) {
                return false;
            }

            /*
             * If this transaction is already synced, remove it from the
             * memory, otherwise set commit point of this transaction.
             */
            if (getEntryPoint() > changeSet.getLastSyncEnd()) {
                txn.setCommitPoint(getEntryPoint());
                resetReadStart(true);
                return true;
            } else {
                txn.clear();
                txns.remove(txn.getTransactionId());
            }

            return false;
        }

        /* Reset the lastLsn if the reader reads a larger lsn. */
        private void resetReadStart(boolean commitEntry) {
            if (readStart < reader.getLastLsn()) {
                readStart = reader.getLastLsn();
                if (commitEntry) {

                    /*
                     * Increase the readStart by the entry size of last
                     * transaction commit, so that the LNFileReader won't read
                     * the same transaction twice.
                     */
                    readStart += reader.getLastEntrySize();
                }
            }
        }

        /*
         * Get the position of a log entry on the log, return the VLSN if it's
         * replicated, return LSN if it's standalone.
         */
        private long getEntryPoint() {
            return (envImpl.isReplicated()) ?
                   reader.getVLSN() : reader.getLastLsn();
        }
    }

    public Iterator<ChangeTxn> getChangeTxns() {
        return new LogChangeIterator();
    }

    /* Reset the LogChangeSet.nextSyncStart. */
    private void resetChangeSetNextSyncStart() {
        if (txns.size() == 0) {

            /*
             * All transaction commits, the nextSyncStart should be the same as
             * lastSyncEnd.
             */
            changeSet.setNextSyncStart(changeSet.getLastSyncEnd());
        } else {

            /*
             * txns is a LinkedHashMap, so the startPoint of first entry in
             * txns should be the LogChangeSet.nextSyncStart.
             */
             for (ChangeTxn changeTxn : txns.values()) {
                 changeSet.setNextSyncStart
                     (((JEChangeTxn) changeTxn).getStartPoint());
                 break;
             }
        }
    }

    /**
     * @hidden
     * Used by tests only.
     */
    public void setWaitHook(TestHook waitHook) {
        this.waitHook = waitHook;
    }

    public void discardChanges(Transaction txn) {
        synchronized (envImpl.getSyncCleanerBarrier()) {
            TestHookExecute.doHookIfSet(waitHook);

            /* Set the LogChangeSet.lastSyncEnd. */
            if (lastSyncEnd > changeSet.getLastSyncEnd()) {
                changeSet.setLastSyncEnd(lastSyncEnd);
            }

            resetChangeSetNextSyncStart();
            writeSyncDB(txn);
        }
    }
       
    private void writeSyncDB(Transaction txn) {
        DatabaseEntry data = new DatabaseEntry();
        binding.objectToEntry(changeSet, data);

        processor.writeChangeSetData(txn, dataSetName, data);
    }
}
TOP

Related Classes of com.sleepycat.je.sync.impl.LogChangeReader$LogChangeIterator

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.