/******************************************************************************* * ALMA - Atacama Large Millimeter Array * Copyright (c) COSYLAB - Control System Laboratory, 2011 * (in the framework of the ALMA collaboration). * All rights reserved. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *******************************************************************************/ package com.cosylab.logging.client.cache; import java.io.IOException; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import java.util.TreeMap; import com.cosylab.logging.engine.log.ILogEntry; /** * This class extends the LogFileCache adding the buffering of the logs * that must be written on disk. * * It uses a WriteBuffer to store the logs to write on disk. * The purpose of this class is to write several logs at once reducing * the write operations on disk. * * The buffer stores the logs when they are added. * It flushes the buffer on disk when the number of logs in memory * exceeds the maximum size. This means that the number of chars written on * the file cache at once is not fixed but depends on the lengths of the * logs stored in this buffer. * To enhance performance and to have the size of the file always available, * the buffer stores the log together with other info in Map of BufferedCacheItem * * @author acaproni */ public class LogBufferedFileCache extends LogFileCache implements ILogMap { /** * Objects of this class represent an entry of the buffer. * For each log, the string to write on disk is also stored * This allows * 1 to know the size of the file on disk taking in account the size * of the buffer * 2 to convert only once the log in a cache string * * @author acaproni * */ private static class BufferedCacheItem { private ILogEntry logEntry; private String logCacheString; public BufferedCacheItem(ILogEntry log, String cacheString) { if (log==null) { throw new IllegalArgumentException("Invalid null log entry"); } if (cacheString==null || cacheString.length()==0) { throw new IllegalArgumentException("Invalid log cache string "+cacheString); } logEntry=log; logCacheString= cacheString; } /** * Getter * * @return The log entry */ public ILogEntry getLogEntry() { return logEntry; } /** * Getter * * @return The string to write on disk */ public String getLogCacheString() { return logCacheString; } } public static final String WRITEBUFFERSIZE_PROPERTY_NAME = "jlog.cache.writebuffersize"; public static final int DEFAULT_WRITEBUFFERSIZE = 8192; // The buffer of logs is a TreeMap having has key the identifier // of the log and a BufferedCacheItem as value private TreeMap<Integer,BufferedCacheItem> buffer= new TreeMap<Integer,BufferedCacheItem>(); // The capacity of the buffer (measured as number of logs): // when the buffer is full it is flushed on disk private int size; // The size of the chars in the buffer // // It is the length of the string to write on disk containing all the logs // translated in the cache format. // This allows to know the size of the file on disk taking into account // the size of the logs in the buffer private volatile long bufferFileSize=0; /** * Build a LogBufferedFileCache with the given size for the cache and the * write buffer. * If there isn't enough memory for the cache, tries with a smmaller * size. * * @param cacheSize The size of the cache * @param writeBufferSize The size of the write buffer * @throws LogCacheException If there is'nt enough memory for a buffer of at least 32 logs * */ public LogBufferedFileCache(int writeBufferSize) throws LogCacheException { if (writeBufferSize<=0) { throw new IllegalArgumentException("Invalid size for the buffer "+writeBufferSize); } size = writeBufferSize; } /** * Build a LogBufferedFileCache with the default sizes for * the write buffer * * @throws LogCacheException * * @see LogBufferedFileCache.getDefaultWriteCacheSize */ public LogBufferedFileCache() throws LogCacheException { this(getDefaultWriteCacheSize()); } /** * Empty the cache * * @throws IOException */ public synchronized void clear() throws LogCacheException { super.clear(); synchronized(buffer) { buffer.clear(); } bufferFileSize=0; } /** * Return the log with the given key * * @param key The key of the log * @return The LogEntryXML or null in case of error */ public synchronized ILogEntry getLog(Integer key) throws LogCacheException { BufferedCacheItem item = buffer.get(key); if (item==null) { return super.getLog(key); } return item.getLogEntry(); } /** * Delete a log with the given key * * @param pos The key of the log to delete */ public synchronized void deleteLog(Integer key) throws LogCacheException { BufferedCacheItem itemToDelete = buffer.get(key); if (itemToDelete!=null) { synchronized (buffer) { buffer.remove(key); } bufferFileSize-=itemToDelete.getLogCacheString().length(); } else { super.deleteLog(key); } } /** * Delete a collection of logs * * @param keys The keys of the logs to remove from the cache */ public synchronized void deleteLogs(Collection<Integer> keys) throws LogCacheException { if (keys==null) { throw new IllegalArgumentException("Illegal null collection of logs to delete"); } Iterator<Integer> iter =keys.iterator(); while (iter.hasNext()) { Integer key = iter.next(); BufferedCacheItem cacheItem; synchronized(buffer) { cacheItem=buffer.remove(key); } if (cacheItem!=null) { iter.remove(); bufferFileSize-=cacheItem.getLogCacheString().length(); } } if (keys.size()>0) { super.deleteLogs(keys); } } /** * Append a log to the cache * * @param log The log to append in the cache * @return The key of the added log */ public synchronized int add(ILogEntry log) throws LogCacheException { if (log==null) { throw new LogCacheException("Error: trying to add a null log to the buffer"); } int logInBuffer; BufferedCacheItem cacheItem = new BufferedCacheItem(log,toCacheString(log)); synchronized (buffer) { buffer.put(logID,cacheItem); logInBuffer=buffer.size(); bufferFileSize+=cacheItem.getLogCacheString().length(); } if (logInBuffer==size) { flushBuffer(); } return logID++; } /** * Gets the default size ot the write buffer, which comes either from the system property * <code>jlog.cache.writebuffersize</code> (see {@link #WRITEBUFFERSIZE_PROPERTY_NAME}) * or, if this property is undefined or invalid, from the fixed size given by * {@link #DEFAULT_WRITEBUFFERSIZE}. * * @return the default size of the write buffer to be used if none is given in the constructor */ private static int getDefaultWriteCacheSize() { Integer cacheSizeFromProperty = Integer.getInteger(WRITEBUFFERSIZE_PROPERTY_NAME); if (cacheSizeFromProperty != null) { return cacheSizeFromProperty.intValue(); } return DEFAULT_WRITEBUFFERSIZE; } /** * Flush all the logs on file * */ public synchronized void flushBuffer() throws LogCacheException { // str is the buffer of logs to write on disk at once StringBuilder str = new StringBuilder(); if (file==null) { try { initCache(); } catch (IOException e) { throw new LogCacheException("Error flushing buffer",e); } } // The position of the first log to add on disk long startingPos; try { startingPos = file.length(); } catch (IOException ioe) { throw new LogCacheException("Error getting the length of the file ",ioe); } // Prepare the buffer and the index LogFileCache.LogCacheInfo info; for (Integer key: buffer.keySet()) { BufferedCacheItem item = buffer.get(key); info = new LogFileCache.LogCacheInfo(); info.start=startingPos+str.length(); str.append(item.getLogCacheString()); info.len=item.getLogCacheString().length(); if (buffer.containsKey(key)) { index.put(key,info); } } // Write the buffer on disk synchronized (file) { try { file.seek(startingPos); file.writeBytes(str.toString()); } catch (IOException ioe) { throw new LogCacheException("Error writing the buffer on disk",ioe); } } // Clear the buffer synchronized (buffer) { buffer.clear(); } bufferFileSize=0; } /** * Return the number of logs in cache * * @return The number of logs in cache */ public synchronized int getSize() { int sz; synchronized (buffer) { sz=buffer.size(); } return super.getSize()+sz; } /** * Return the length of the file on disk taking into account * the length of the string to write on the disk for the logs in the * buffer. * * @return The size of the file cache * * @throws IOException in case of I/O error * @see java.io.RandomAccessFile */ public long getFileSize() throws IOException{ return super.getFileSize()+bufferFileSize; } /** * * @return The number of logs actually in the buffer */ public synchronized final int getBufferSize() { return buffer.size(); } /** * Return the key of the first valid log (FIFO). * The key of the first log is 0 but it can change if the log 0 has * been deleted. * * @return The key of the first log * null if the cache is empty */ public Integer getFirstLog() { Integer cacheFirstLog = super.getFirstLog(); Integer bufferFirstLog; synchronized(buffer) { if (buffer.isEmpty()) { return cacheFirstLog; } else { bufferFirstLog=buffer.firstKey(); } } if (cacheFirstLog==null) { return bufferFirstLog; } else { return Math.min(cacheFirstLog,bufferFirstLog); } } /** * Append at most n keys from the first valid logs to the collection. * First here means first in the FIFO policy. * * The number of added keys can be less then n if the cache doesn't * contain enough logs. * * @param n The desired number of keys of first logs * @param keys The collection to add they keys to * @return The number of keys effectively added */ public int getFirstLogs(int n, Collection<Integer> keys) { if (n<=0 || keys==null) { throw new IllegalArgumentException("Invalid number of requested keys or null collection"); } int ret=super.getFirstLogs(n,keys); if (ret<n) { Set<Integer> allTheKeys = buffer.keySet(); Iterator<Integer> iter = allTheKeys.iterator(); while (iter.hasNext() && ret<n) { keys.add(iter.next()); ret++; } } return ret; } /** * Return the key of the last valid log (FIFO) * The key of the last log is the key of the last inserted log * but it can cheang if such log has been deleted * * @return The key of the last inserted log * null if th cache is empty */ public Integer getLastLog() { Integer bufferLastLog; Integer cacheLastLog = super.getLastLog(); synchronized(buffer) { if (buffer.isEmpty()) { return cacheLastLog; } else { bufferLastLog=buffer.lastKey(); } } if (cacheLastLog==null) { return bufferLastLog; } else { return Math.max(cacheLastLog,bufferLastLog); } } /** * Return a set with all the keys of the logs in cache * * @return The keys of the logs in cache */ public Set<Integer> keySet() { HashSet<Integer> ret = new HashSet<Integer>(); ret.addAll(super.keySet()); ret.addAll(buffer.keySet()); return ret; } /** * Return an iterator over the logs in cache */ public Iterator<ILogEntry> iterator() { return new LogIterator(this); } /** * Replace the log in the given position with the new one * <P> * If the log to replace is not in the buffer, <code>LogBufferedFileCache</code> * delegated to <code>LogFileCache</code> * @param position The position of the log to replace * @param log The key (identifier) of the log */ @Override public synchronized void replaceLog(Integer key, ILogEntry log) throws LogCacheException { synchronized (buffer) { if (buffer.containsKey(key)) { BufferedCacheItem oldItem=buffer.get(key); BufferedCacheItem cacheItem = new BufferedCacheItem(log,toCacheString(log)); buffer.put(key,cacheItem); bufferFileSize=bufferFileSize+cacheItem.getLogCacheString().length()-oldItem.getLogCacheString().length(); return; } } super.replaceLog(key, log); } }