Package org.fcrepo.server.journal.readerwriter.multicast

Source Code of org.fcrepo.server.journal.readerwriter.multicast.MulticastJournalWriter

/* The contents of this file are subject to the license and copyright terms
* detailed in the license directory at the root of the source tree (also
* available online at http://fedora-commons.org/license/).
*/

package org.fcrepo.server.journal.readerwriter.multicast;

import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;

import javax.xml.stream.XMLEventWriter;

import org.fcrepo.server.errors.ServerException;
import org.fcrepo.server.journal.JournalException;
import org.fcrepo.server.journal.JournalOperatingMode;
import org.fcrepo.server.journal.JournalWriter;
import org.fcrepo.server.journal.ServerInterface;
import org.fcrepo.server.journal.entry.CreatorJournalEntry;
import org.fcrepo.server.journal.helpers.JournalHelper;
import org.fcrepo.server.journal.helpers.ParameterHelper;
import org.fcrepo.server.journal.readerwriter.multicast.request.CloseFileRequest;
import org.fcrepo.server.journal.readerwriter.multicast.request.OpenFileRequest;
import org.fcrepo.server.journal.readerwriter.multicast.request.ShutdownRequest;
import org.fcrepo.server.journal.readerwriter.multicast.request.TransportRequest;
import org.fcrepo.server.journal.readerwriter.multicast.request.WriteEntryRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static org.fcrepo.server.journal.readerwriter.multicast.Transport.State.FILE_CLOSED;
import static org.fcrepo.server.journal.readerwriter.multicast.Transport.State.FILE_OPEN;
import static org.fcrepo.server.journal.readerwriter.multicast.Transport.State.SHUTDOWN;


/**
* SYNCHRONIZATION NOTE: All public methods are synchronized against
* {@link JournalWriter.SYNCHRONIZER}, as is the {@link #closeFile() closeFile}
* method. This means that an asynchronous call by the timer task will not
* interrupt a synchronous operation already in progress, or vice versa.
*
* @author jblake
*/
public class MulticastJournalWriter
        extends JournalWriter
        implements TransportParent {

    private static final Logger logger =
            LoggerFactory.getLogger(MulticastJournalWriter.class);

    /**
     * prefix that indicates a transport parameter - must include the separator
     * character, if one is expected.
     */
    public static final String TRANSPORT_PARAMETER_PREFIX = "transport.";

    /**
     * Required parameter for each transport: the full name of the class that
     * implements the transport.
     */
    public static final String CLASSNAME_PARAMETER_KEY = "classname";

    /**
     * Required parameter for each transport, and must be set to "true" on at
     * least one transport.
     */
    public static final String CRUCIAL_PARAMETER_KEY = "crucial";

    /**
     * Every Transport needs these types of arguments for its constructor.
     */
    private static final Class<?>[] TRANSPORT_CONSTRUCTOR_ARGUMENT_TYPES =
            new Class<?>[] {Map.class, Boolean.TYPE, TransportParent.class};

    /** Journal file names will start with this string. */
    private final String filenamePrefix;

    /** Number of bytes before we start a new file - 0 means no limit */
    private final long sizeLimit;

    /** Number of milliseconds before we start a new file - 0 means no limit */
    private final long ageLimit;

    /** Nested map of parameters, keyed by transport name. */
    private final Map<String, Map<String, String>> transportParameters;

    /** Map of the transports, keyed by transport name. */
    private final Map<String, Transport> transports;

    /** Current state of the writer and the transports. */
    private Transport.State state = FILE_CLOSED;

    /** Approximately how many bytes have been written to the current file? */
    private long currentSize;

    /** A tool to estimate the output size of a JournalEntry. */
    private final JournalEntrySizeEstimator sizeEstimator;

    /** A timer to monitors the age of the current file. */
    private Timer timer;

    public MulticastJournalWriter(Map<String, String> parameters,
                                  String role,
                                  ServerInterface server)
            throws JournalException {
        super(parameters, role, server);

        filenamePrefix =
                ParameterHelper.parseParametersForFilenamePrefix(parameters);
        sizeLimit = ParameterHelper.parseParametersForSizeLimit(parameters);
        ageLimit = ParameterHelper.parseParametersForAgeLimit(parameters);

        transportParameters = parseTransportParameters(parameters);
        checkTransportParametersForValidity();
        transports = createTransports();

        sizeEstimator = new JournalEntrySizeEstimator(this);
    }

    /**
     * Create a Map of Maps, holding parameters for all of the transports.
     *
     * @throws JournalException
     */
    private Map<String, Map<String, String>> parseTransportParameters(Map<String, String> parameters)
            throws JournalException {
        Map<String, Map<String, String>> allTransports =
                new LinkedHashMap<String, Map<String, String>>();
        for (String key : parameters.keySet()) {
            if (isTransportParameter(key)) {
                Map<String, String> thisTransport =
                        getThisTransportMap(allTransports,
                                            getTransportName(key));
                thisTransport.put(getTransportParameterName(key), parameters
                        .get(key));
            }
        }
        return allTransports;
    }

    private boolean isTransportParameter(String key) throws JournalException {
        return key.startsWith(TRANSPORT_PARAMETER_PREFIX);
    }

    private int findParameterNameSeparator(String key) throws JournalException {
        int dotHere = key.indexOf('.', TRANSPORT_PARAMETER_PREFIX.length());
        if (dotHere < 0) {
            throw new JournalException("Invalid name for transport parameter '"
                    + key + "' - requires '.' after transport name.");
        }
        return dotHere;
    }

    private String getTransportParameterName(String key)
            throws JournalException {
        return key.substring(findParameterNameSeparator(key) + 1);
    }

    private String getTransportName(String key) throws JournalException {
        return key.substring(TRANSPORT_PARAMETER_PREFIX.length(),
                             findParameterNameSeparator(key));
    }

    /** If we don't yet have a map for this transport name, create one. */
    private Map<String, String> getThisTransportMap(Map<String, Map<String, String>> allTransports,
                                                    String transportName) {
        if (!allTransports.containsKey(transportName)) {
            allTransports.put(transportName, new HashMap<String, String>());
        }
        return allTransports.get(transportName);
    }

    /** "protected" so we can mock it out in unit tests. */
    protected void checkTransportParametersForValidity()
            throws JournalException {
        checkAtLeastOneTransport();
        checkAllTransportsHaveClassnames();
        checkAllTransportsHaveCrucialFlags();
        checkAtLeastOneCrucialTransport();
        logger.info("Journal transport parameters validated.");
    }

    private void checkAtLeastOneTransport() throws JournalException {
        if (transportParameters.size() == 0) {
            throw new JournalException("MulticastJournalWriter must have "
                    + "at least one Transport.");
        }
    }

    private void checkAllTransportsHaveClassnames() throws JournalException {
        for (String transportName : transportParameters.keySet()) {
            Map<String, String> thisTransportMap =
                    transportParameters.get(transportName);
            if (!thisTransportMap.containsKey(CLASSNAME_PARAMETER_KEY)) {
                throw new JournalException("Transport '" + transportName
                        + "' does not have a '" + CLASSNAME_PARAMETER_KEY
                        + "' parameter");
            }
        }
    }

    private void checkAllTransportsHaveCrucialFlags() throws JournalException {
        for (String transportName : transportParameters.keySet()) {
            Map<String, String> thisTransportMap =
                    transportParameters.get(transportName);
            if (!thisTransportMap.containsKey(CRUCIAL_PARAMETER_KEY)) {
                throw new JournalException("Transport '" + transportName
                        + "' does not have a '" + CRUCIAL_PARAMETER_KEY
                        + "' parameter");
            }
        }
    }

    private void checkAtLeastOneCrucialTransport() throws JournalException {
        for (String transportName : transportParameters.keySet()) {
            Map<String, String> thisTransportMap =
                    transportParameters.get(transportName);
            String crucialString = thisTransportMap.get(CRUCIAL_PARAMETER_KEY);
            if (Boolean.parseBoolean(crucialString)) {
                return;
            }
        }
        throw new JournalException("There must be at least one crucial transport.");
    }

    private Map<String, Transport> createTransports() throws JournalException {
        Map<String, Transport> result = new HashMap<String, Transport>();
        for (String transportName : transportParameters.keySet()) {
            Map<String, String> thisTransportMap =
                    transportParameters.get(transportName);
            String className = thisTransportMap.get(CLASSNAME_PARAMETER_KEY);
            boolean crucialFlag =
                    Boolean.parseBoolean(thisTransportMap
                            .get(CRUCIAL_PARAMETER_KEY));

            Object transport =
                    JournalHelper
                            .createInstanceFromClassname(className,
                                                         TRANSPORT_CONSTRUCTOR_ARGUMENT_TYPES,
                                                         new Object[] {
                                                                 thisTransportMap,
                                                                 crucialFlag,
                                                                 this});
            logger.info("Transport '" + transportName + "' is " + transport);
            result.put(transportName, (Transport) transport);
        }
        return result;
    }

    Map<String, Transport> getTransports() {
        return transports;
    }

    /**
     * <p>
     * Get ready to write a journal entry, insuring that we have an open file.
     * </p>
     * <p>
     * If we are shutdown, ignore this request. Otherwise, check if we need to
     * shut a file down based on size limit. Then check to see whether we need
     * to open another file. If so, we'll need a repository hash and a filename.
     * </p>
     *
     * @see org.fcrepo.server.journal.JournalWriter#prepareToWriteJournalEntry()
     */
    @Override
    public void prepareToWriteJournalEntry() throws JournalException {
        synchronized (JournalWriter.SYNCHRONIZER) {
            if (state == SHUTDOWN) {
                return;
            }

            logger.debug("Preparing to write journal entry.");

            if (state == FILE_OPEN) {
                closeFileIfAppropriate();
            }

            if (state == FILE_CLOSED) {
                openNewFile();
            }
        }
    }

    /**
     * <p>
     * Write a journal entry.
     * </p>
     * <p>
     * If we are shutdown, ignore this request. Otherwise, get an output stream
     * from each Transport in turn, and write the entry. If this puts the file
     * size over the limit, close them.
     * </p>
     *
     * @see org.fcrepo.server.journal.JournalWriter#writeJournalEntry(org.fcrepo.server.journal.entry.CreatorJournalEntry)
     */
    @Override
    public void writeJournalEntry(CreatorJournalEntry journalEntry)
            throws JournalException {
        synchronized (JournalWriter.SYNCHRONIZER) {
            if (state == SHUTDOWN) {
                return;
            }
            logger.debug("Writing journal entry.");
            sendRequestToAllTransports(new WriteEntryRequest(this, journalEntry));
            currentSize += sizeEstimator.estimateSize(journalEntry);

            if (state == FILE_OPEN) {
                closeFileIfAppropriate();
            }
        }
    }

    /**
     * <p>
     * Shut it down
     * </p>
     * <p>
     * If the Transports still have files open, close them. Then stop responding
     * to requests.
     * </p>
     *
     * @see org.fcrepo.server.journal.JournalWriter#shutdown()
     */
    @Override
    public void shutdown() throws JournalException {
        synchronized (JournalWriter.SYNCHRONIZER) {
            if (state == SHUTDOWN) {
                return;
            }
            if (state == FILE_OPEN) {
                closeFile();
            }

            logger.debug("Shutting down.");
            sendRequestToAllTransports(new ShutdownRequest());
            state = SHUTDOWN;
        }
    }

    private void openNewFile() throws JournalException {
        try {
            String hash = server.getRepositoryHash();
            String filename =
                    JournalHelper.createTimestampedFilename(filenamePrefix,
                                                            getCurrentDate());
            timer = createTimer();
            sendRequestToAllTransports(new OpenFileRequest(hash,
                                                           filename,
                                                           getCurrentDate()));
            currentSize = 0;
            state = FILE_OPEN;
        } catch (ServerException e) {
            throw new JournalException(e);
        }
    }

    /** protected, so it can be mocked out for unit testing. */
    protected Date getCurrentDate() {
        return new Date();
    }

    /**
     * Create the timer, and schedule a task that will let us know when the file
     * is too old to continue. If the age limit is 0 or negative, we treat it as
     * "no limit".
     */
    private Timer createTimer() {
        Timer fileTimer = new Timer();

        // if the age limit is 0 or negative, treat it as "no limit".
        if (ageLimit >= 0) {
            fileTimer.schedule(new CloseFileTimerTask(), ageLimit);
        }

        return fileTimer;
    }

    /**
     * When the timer goes off, close the file.
     */
    private final class CloseFileTimerTask
            extends TimerTask {

        @Override
        public void run() {
            try {
                logger.debug("Timer task requests file close.");
                closeFile();
            } catch (JournalException e) {
                /*
                 * What to do with this exception? If we print it, where is the
                 * console? If we throw it, who will catch it?
                 */
                e.printStackTrace();
                throw new IllegalStateException(e);
            }
        }
    }

    /**
     * Check to see whether the file size has passed the limit.
     */
    private void closeFileIfAppropriate() throws JournalException {
        if (sizeLimit != 0 && currentSize >= sizeLimit) {
            closeFile();
        }
    }

    /**
     * Close the file unconditionally. Called if
     * <ul>
     * <li>the file passes the size limit,</li>
     * <li>the timer expires,</li>
     * <li>the server commands a shutdown</li>
     * </ul>
     * Synchronized so a close request from the timer doesn't conflict with
     * other processing.
     */
    private void closeFile() throws JournalException {
        synchronized (JournalWriter.SYNCHRONIZER) {
            // check to be sure that another thread didn't close the file while
            // we were waiting for the lock.
            if (state == FILE_OPEN) {
                sendRequestToAllTransports(new CloseFileRequest());
                currentSize = 0;
                state = FILE_CLOSED;
            }

            // turn off the timer that is checking the age of this file.
            if (timer != null) {
                timer.cancel();
            }
        }
    }

    /** make this public, so the TransportRequest class can call it. */
    @Override
    public void writeJournalEntry(CreatorJournalEntry journalEntry,
                                  XMLEventWriter writer)
            throws JournalException {
        super.writeJournalEntry(journalEntry, writer);
    }

    /**
     * make this public so the Transport classes can call it via
     * TransportParent.
     */
    @Override
    public void writeDocumentHeader(XMLEventWriter writer,
                                    String repositoryHash,
                                    Date currentDate) throws JournalException {
        super.writeDocumentHeader(writer, repositoryHash, currentDate);
    }

    /**
     * make this public so the Transport classes can call it via
     * TransportParent.
     */
    @Override
    public void writeDocumentTrailer(XMLEventWriter writer)
            throws JournalException {
        super.writeDocumentTrailer(writer);
    }

    /**
     * Send a request for some operation to the Transports. Send it to all of
     * them, even if one or more throws an Exception. Report any exceptions when
     * all Transports have been attempted.
     *
     * @param request
     *        the request object
     * @param args
     *        the arguments to be passed to the request object
     * @throws JournalException
     *         if there were any crucial problems.
     */
    private void sendRequestToAllTransports(TransportRequest request)
            throws JournalException {
        Map<String, JournalException> crucialExceptions =
                new LinkedHashMap<String, JournalException>();
        Map<String, JournalException> nonCrucialExceptions =
                new LinkedHashMap<String, JournalException>();

        /*
         * Send the request to all transports, accumulating any Exceptions as we
         * go. That way, we increase the likeihood that at least one Transport
         * succeeded in the request.
         */
        for (String transportName : transports.keySet()) {
            Transport transport = transports.get(transportName);
            try {
                logger.debug("Sending " + request.getClass().getSimpleName()
                        + " to transport '" + transportName + "'");
                request.performRequest(transport);
            } catch (JournalException e) {
                if (transport.isCrucial()) {
                    crucialExceptions.put(transportName, e);
                } else {
                    nonCrucialExceptions.put(transportName, e);
                }
            }
        }

        /*
         * Report the Exceptions. Report the non-crucial ones first, in case the
         * Server decides to take some definitive action on a crucial Exception.
         */
        reportNonCrucialExceptions(nonCrucialExceptions);
        reportCrucialExceptions(crucialExceptions);
    }

    private void reportNonCrucialExceptions(Map<String, JournalException> nonCrucialExceptions) {
        if (nonCrucialExceptions.isEmpty()) {
            return;
        }
        for (String transportName : nonCrucialExceptions.keySet()) {
            JournalException e = nonCrucialExceptions.get(transportName);
            logger.error("Exception thrown from non-crucial Journal Transport: '"
                    + transportName + "'", e);
        }
    }

    private void reportCrucialExceptions(Map<String, JournalException> crucialExceptions)
            throws JournalException {
        if (!crucialExceptions.isEmpty()) {
            JournalOperatingMode.setMode(JournalOperatingMode.READ_ONLY);
        }
        for (String transportName : crucialExceptions.keySet()) {
            JournalException e = crucialExceptions.get(transportName);
            logger.error("Exception thrown from crucial Journal Transport: '"
                    + transportName + "'", e);
        }
    }

}
TOP

Related Classes of org.fcrepo.server.journal.readerwriter.multicast.MulticastJournalWriter

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.