/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt * or http://forgerock.org/license/CDDLv1.0.html. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at legal-notices/CDDLv1_0.txt. * If applicable, add the following below this CDDL HEADER, with the * fields enclosed by brackets "[]" replaced with your own identifying * information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * * Copyright 2013-2015 ForgeRock AS */ package org.opends.server.replication.server.changelog.file; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.TreeMap; import org.opends.server.replication.common.CSN; import org.opends.server.replication.protocol.UpdateMsg; import org.opends.server.replication.server.changelog.api.ChangelogException; import org.opends.server.replication.server.changelog.api.DBCursor; import org.opends.server.util.StaticUtils; /** * {@link DBCursor} implementation that iterates across a Collection of * {@link DBCursor}s, advancing from the oldest to the newest change cross all * cursors. * * @param <T> * The type of data associated with each cursor * \@NotThreadSafe */ abstract class CompositeDBCursor<T> implements DBCursor<UpdateMsg> { private static final byte UNINITIALIZED = 0; private static final byte READY = 1; private static final byte CLOSED = 2; /** The state of this cursor. One of {@link #UNINITIALIZED}, {@link #READY} or {@link #CLOSED} */ private byte state = UNINITIALIZED; /** * These cursors are considered exhausted because they had no new changes the * last time {@link DBCursor#next()} was called on them. Exhausted cursors * might be recycled at some point when they start returning changes again. */ private final Map<DBCursor<UpdateMsg>, T> exhaustedCursors = new HashMap<>(); /** * The cursors are sorted based on the current change of each cursor to * consider the next change across all available cursors. * <p> * New cursors for this Map must be created from the same thread that will * make use of them. When this rule is not obeyed, a JE exception will be * thrown about * "Non-transactional Cursors may not be used in multiple threads;". */ private final TreeMap<DBCursor<UpdateMsg>, T> cursors = new TreeMap<>( new Comparator<DBCursor<UpdateMsg>>() { @Override public int compare(DBCursor<UpdateMsg> o1, DBCursor<UpdateMsg> o2) { final CSN csn1 = o1.getRecord().getCSN(); final CSN csn2 = o2.getRecord().getCSN(); int cmpCsn = CSN.compare(csn1, csn2); if (cmpCsn == 0 && o1 instanceof CompositeDBCursor && o2 instanceof CompositeDBCursor) { // Ensures a consistent order when the CSNs are equal (rare in practice) T data1 = ((CompositeDBCursor<T>) o1).getData(); T data2 = ((CompositeDBCursor<T>) o1).getData(); if (data1 instanceof Comparable && data2 instanceof Comparable) { return ((Comparable<T>) data1).compareTo(data2); } } return cmpCsn; } }); /** {@inheritDoc} */ @Override public boolean next() throws ChangelogException { if (state == CLOSED) { return false; } // If previous state was ready, then we must advance the first cursor // (which UpdateMsg has been consumed). // To keep consistent the cursors' order in the SortedSet, it is necessary // to remove the first cursor, then add it again after moving it forward. final Entry<DBCursor<UpdateMsg>, T> cursorToAdvance = state != UNINITIALIZED ? cursors.pollFirstEntry() : null; state = READY; recycleExhaustedCursors(); if (cursorToAdvance != null) { addCursor(cursorToAdvance.getKey(), cursorToAdvance.getValue()); } incorporateNewCursors(); return !cursors.isEmpty(); } private void recycleExhaustedCursors() throws ChangelogException { if (!exhaustedCursors.isEmpty()) { // try to recycle exhausted cursors in case the underlying replica DBs received new changes. final Map<DBCursor<UpdateMsg>, T> copy = new HashMap<>(exhaustedCursors); exhaustedCursors.clear(); for (Entry<DBCursor<UpdateMsg>, T> entry : copy.entrySet()) { addCursor(entry.getKey(), entry.getValue()); } } } /** * Removes the cursor matching the provided data. * * @param dataToFind * the data for which the cursor must be found and removed */ protected void removeCursor(final T dataToFind) { removeCursor(this.cursors, dataToFind); removeCursor(this.exhaustedCursors, dataToFind); } private void removeCursor(Map<DBCursor<UpdateMsg>, T> cursors, T dataToFind) { for (Iterator<Entry<DBCursor<UpdateMsg>, T>> cursorIter = cursors.entrySet().iterator(); cursorIter.hasNext();) { final Entry<DBCursor<UpdateMsg>, T> entry = cursorIter.next(); if (dataToFind.equals(entry.getValue())) { entry.getKey().close(); cursorIter.remove(); } } } /** * Adds a cursor to this composite cursor. It first calls * {@link DBCursor#next()} to verify whether it is exhausted or not. * * @param cursor * the cursor to add to this composite * @param data * the data associated to the provided cursor * @throws ChangelogException * if a database problem occurred */ protected void addCursor(final DBCursor<UpdateMsg> cursor, final T data) throws ChangelogException { if (cursor.next()) { this.cursors.put(cursor, data); } else { this.exhaustedCursors.put(cursor, data); } } /** {@inheritDoc} */ @Override public UpdateMsg getRecord() { // Cannot call incorporateNewCursors() here because // somebody might have already called DBCursor.getRecord() and read the record final Entry<DBCursor<UpdateMsg>, T> entry = cursors.firstEntry(); if (entry != null) { return entry.getKey().getRecord(); } return null; } /** * Called when implementors should incorporate new cursors into the current * composite DBCursor. Implementors should call * {@link #addCursor(DBCursor, Object)} to do so. * * @throws ChangelogException * if a database problem occurred */ protected abstract void incorporateNewCursors() throws ChangelogException; /** * Returns the data associated to the cursor that returned the current record. * * @return the data associated to the cursor that returned the current record. */ public T getData() { final Entry<DBCursor<UpdateMsg>, T> entry = cursors.firstEntry(); if (entry != null) { return entry.getValue(); } return null; } @Override public void close() { state = CLOSED; StaticUtils.close(cursors.keySet()); StaticUtils.close(exhaustedCursors.keySet()); cursors.clear(); exhaustedCursors.clear(); } @Override public String toString() { return getClass().getSimpleName() + " openCursors=" + cursors + " exhaustedCursors=" + exhaustedCursors; } }