/* 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 fedora.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.apache.log4j.Logger; import fedora.server.errors.ServerException; import fedora.server.journal.JournalException; import fedora.server.journal.JournalOperatingMode; import fedora.server.journal.JournalWriter; import fedora.server.journal.ServerInterface; import fedora.server.journal.entry.CreatorJournalEntry; import fedora.server.journal.helpers.JournalHelper; import fedora.server.journal.helpers.ParameterHelper; import fedora.server.journal.readerwriter.multicast.request.CloseFileRequest; import fedora.server.journal.readerwriter.multicast.request.OpenFileRequest; import fedora.server.journal.readerwriter.multicast.request.ShutdownRequest; import fedora.server.journal.readerwriter.multicast.request.TransportRequest; import fedora.server.journal.readerwriter.multicast.request.WriteEntryRequest; import static fedora.server.journal.readerwriter.multicast.Transport.State.FILE_CLOSED; import static fedora.server.journal.readerwriter.multicast.Transport.State.FILE_OPEN; import static fedora.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 LOG = Logger.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(); LOG.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}); LOG.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 fedora.server.journal.JournalWriter#prepareToWriteJournalEntry() */ @Override public void prepareToWriteJournalEntry() throws JournalException { synchronized (JournalWriter.SYNCHRONIZER) { if (state == SHUTDOWN) { return; } LOG.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 fedora.server.journal.JournalWriter#writeJournalEntry(fedora.server.journal.entry.CreatorJournalEntry) */ @Override public void writeJournalEntry(CreatorJournalEntry journalEntry) throws JournalException { synchronized (JournalWriter.SYNCHRONIZER) { if (state == SHUTDOWN) { return; } LOG.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 fedora.server.journal.JournalWriter#shutdown() */ @Override public void shutdown() throws JournalException { synchronized (JournalWriter.SYNCHRONIZER) { if (state == SHUTDOWN) { return; } if (state == FILE_OPEN) { closeFile(); } LOG.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 { LOG.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 { LOG.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); LOG.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); LOG.fatal("Exception thrown from crucial Journal Transport: '" + transportName + "'", e); } } }