/* ALMA - Atacama Large Millimiter Array * (c) European Southern Observatory, 2002 * Copyright by ESO (in the framework of the ALMA collaboration) * and Cosylab 2002, 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.Vector; import java.util.concurrent.atomic.AtomicInteger; import com.cosylab.logging.engine.log.ILogEntry; /** * * This class manages a set of LogBufferedFileCache objects allowing the storing of * logs in different files, though from the user point of view the logs are * stored or retrieved from single data store. * * This is due to the fact that files can grow up to a given size and hence setting * a limit on the number of logs that can be collected. * * Logs are added to a file until it reaches a given size, then a new file is created. * Logs have different length and therefore the number of logs in each File Buffered * Cache will be different (see example below). * * Logs are accessible by the user through a keyword (logKey) which is implemented * as an increased running number. On the other hand, within each File Buffered * Cache they have their own numbering system which starts from 0 to the maximum * number of logs. * * Example : * * logKey logCounter Buffered File * Range Cache Range * -------------------------------------------------------- * 0 - 10 11 0 - 10 file1 * 11 - 25 15 0 - 14 file2 * 26 - 32 7 0 - 6 file3 * 33 - 50 18 0 - 18 file4 * * When logs are deleted they are no longer accessible : when all logs stored in a * single file are marked as deleted the corresponding logCounter = 0 and as * consequence the file itself is removed. * * Using the example let's assume we delete the logs with the following * keyword : 26,27,28,31,32, from now on these logs will not be accessible for the user. * * logKey logCounter Buffered File * Range Cache Range * -------------------------------------------------------- * 0 - 10 11 0 - 10 file1 * 11 - 25 15 0 - 14 file2 * 26 - 32 2 0 - 6 file3 * 33 - 50 18 0 - 18 file4 * * If we delete also logs with keywords 29 and 30 the logCounter will be zero * and file3 will be deleted. * * logKey Mapping * When retrieving the log we need to re-map the global log keyword into * the keyword used to add the log to the specific Buffered File Cache. * This is done by simply subtracting the minimum key value from the * logKey. * * Example : Retrieve logKey 20. * logKey 20 is stored in the second Buffered File Cache and it is * accessible with the local key = (20 - 11) = 9 * * * @author mcomin * */ public class LogMultiFileCache implements ILogMap { /** * The name of the property defining the max size of each fileof the cache */ public static final String FILEMAXSIZE_PROPERTY_NAME = "jlog.cache.fileMaxSize"; /** * The default max size of each file of the cache */ public static final long DEFAULT_FILEMAXSIZE = 10000000; /** * Flag activating some info prints */ private boolean debugTrace=false; /** * This vector implement a table whose records are specified by the * class LogFileTableRecord. */ private long fileMaxSize; /** * The vector of objects describing the buffers on disk */ private Vector<MultiFileTableRecord> logFileTable= new Vector<MultiFileTableRecord>(); /** * The number of logs in cache */ private final AtomicInteger logsInCache=new AtomicInteger(0); /** * The ID (i.e. the key) identifying each log */ private int ID=-1; /** * Get the max size of the file out of the system properties or * uses the default value if the property does not exist */ private static long getDefaultMaxFileSize() { Integer fileSizeFromProperty = Integer.getInteger(FILEMAXSIZE_PROPERTY_NAME); if (fileSizeFromProperty != null) { return fileSizeFromProperty.longValue(); } return DEFAULT_FILEMAXSIZE; } /** * Prints trace info if traceDebug flag is true * */ private void printDebugTrace(String traceInfo) { if (debugTrace) { System.out.println(traceInfo); } } /** * Creates a new record for the log file table : min/max log indexes are * initialized according to the total number of received logs. * * The create <code>LogFileTableRecord</code> is added to the table of file records. * */ private MultiFileTableRecord createNewFileRecord() throws LogCacheException { MultiFileTableRecord newFileRec=new MultiFileTableRecord(); // Add new record to vector logFileTable.add(newFileRec); return newFileRec; } /** * Looks in the log file table in which file buffered cache the log has * been stored. * * The search is made by checking the min and max key of the logs contained in each record * against the key of the searched log. * As a consequence the record returned by this method is the only one record that <B>can</B> * contain this log but it could be that the log is not there (for example it has been deleted) * * @param logKey The key of the log to look for * @return The record containing the log * @throws LogCacheException If no record is found or an error happened */ private MultiFileTableRecord searchFileLogTable(Integer logKey) throws LogCacheException { // Check for key validity if (logKey == null || logKey < 0 ) { throw new IllegalArgumentException("Invalid or null log key "+logKey); } // Check if log file table empty if (logFileTable.isEmpty()) { throw new LogCacheException("Empty log file table"); } for (MultiFileTableRecord record: logFileTable) { if (logKey>=record.getMinLogIdx() && logKey<=record.getMaxLogIdx()) { return record; } } throw new LogCacheException("Log record containing "+logKey+" not found"); } /** * Builds a LogMultiFileCache objects setting the maximum size of the * cache files * * @param fileSize The maximum size of cache files */ public LogMultiFileCache(long fileSize) throws LogCacheException { if (fileSize<=0) { throw new IllegalArgumentException("Invalid size for logFile "+fileSize); } fileMaxSize = fileSize; printDebugTrace("LogMultiFileCache uses file of max size: "+fileMaxSize); } /** * Constructor * * The maximum size of the files is read from the system properties. * If the property is not found a default value is used. */ public LogMultiFileCache() throws LogCacheException { this(getDefaultMaxFileSize()); } /** * Retrieves a log by means of its keyword. * * @param pos The key of the log to retrieve */ public ILogEntry getLog(Integer logKey) throws LogCacheException { MultiFileTableRecord fileRecord = searchFileLogTable(logKey); return fileRecord.getLog(logKey); } /** * Add a log to the cache * * @param log The log to add to the cache * */ public synchronized int add(ILogEntry log) throws LogCacheException { if (log==null) { throw new LogCacheException("Trying to add a null log to the buffer"); } // Check if file size exceeds fileMaxSize MultiFileTableRecord fileRecord; if (logFileTable.size()>0) { fileRecord=logFileTable.get(logFileTable.size() - 1); } else { fileRecord = createNewFileRecord(); } long actualSize; try { actualSize = fileRecord.getFileSize(); } catch (IOException e) { throw new LogCacheException("Error getting the size of the file",e); } if (actualSize >= fileMaxSize) { // Move logs from internal buffer to file fileRecord.flushBuffer(); printDebugTrace("LogMultiFileCache log file : fileSize "+actualSize+" logs = "+fileRecord.getNumOfLogs()+ " Range keys = "+fileRecord.getMinLogIdx()+" "+fileRecord.getMaxLogIdx()); // Replace old file record with new one. fileRecord= createNewFileRecord(); } fileRecord.addLog(log, ++ID); logsInCache.incrementAndGet(); return ID; } /** * Delete a log with the given key. * * @param pos The key of the log to delete */ public synchronized void deleteLog(Integer logKey) throws LogCacheException { if (logKey==null) { throw new IllegalArgumentException("The key can't be null"); } MultiFileTableRecord fileRecord = searchFileLogTable(logKey); int idx = logFileTable.indexOf(fileRecord); if (idx==-1) { throw new IllegalStateException("The record is not in the vector"); } // Delete log in the LogBufferedFile cache fileRecord.deleteLog(logKey); // Update table record logsInCache.decrementAndGet(); printDebugTrace("\n Delete log key = "+logKey+" from record "+idx+ " Tot. Logs "+ logsInCache + " Range keys = "+fileRecord.getMinLogIdx()+" "+fileRecord.getMaxLogIdx()); // Remove file and element from the vector only if this is not // the logFile we are using to add logs. if (fileRecord.getNumOfLogs()==0) { fileRecord.clear(); printDebugTrace("Delete log file: recId " + idx + " num. of logs = "+fileRecord.getNumOfLogs()+ " Range keys = [" +fileRecord.getMinLogIdx()+", "+fileRecord.getMaxLogIdx()+"] files in table "+logFileTable.size()); // Remove vector element logFileTable.remove(fileRecord); } } /** * * @return The number of logs in the map * @see com.cosylab.logging.client.cache.ILogMap */ public int getSize() { return logsInCache.get(); } /** * 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 change if such log has been deleted * * @return The key of the last inserted log * null if the cache is empty * @see com.cosylab.logging.client.cache.ILogMap */ public Integer getLastLog() { if (logsInCache.get()==0) { return null; } MultiFileTableRecord lastRecord = logFileTable.lastElement(); Integer key = lastRecord.getLastLog(); if (key==null) { throw new IllegalStateException("The cache is not empty but the last log does not exist?!?!"); } return key; } /** * 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 * @see com.cosylab.logging.client.cache.ILogMap * @throws In case of error getting the first log */ public Integer getFirstLog() { if (logsInCache.get()==0) { return null; } MultiFileTableRecord firstRecord = logFileTable.get(0); Integer key = firstRecord.getFirstLog(); if (key==null) { throw new IllegalStateException("The cache is not empty but the last log does not exist?!?!"); } return key; } /** * Delete a set of logs * * @param keys The keys of logs to delete * @see com.cosylab.logging.client.cache.ILogMap */ public void deleteLogs(Collection<Integer> keys) throws LogCacheException { if (keys==null || keys.size()==0) { return; } for (Integer key: keys) { deleteLog(key); } } /** * Clear the Map i.e. remove all the logs and keys from the map * * @throws LogCacheException * @see com.cosylab.logging.client.cache.ILogMap */ public synchronized void clear() throws LogCacheException { for (MultiFileTableRecord record: logFileTable) { record.clear(); } logFileTable.clear(); logsInCache.set(0); } /** * Replace the log in the given position with the new one * @param position The position of the log to replace * @param log The key (identifier) of the log * @see com.cosylab.logging.client.cache.ILogMap */ public void replaceLog(Integer key, ILogEntry log) throws LogCacheException { MultiFileTableRecord tableRecord=searchFileLogTable(key); tableRecord.replaceLog(key, log); } /** * Return an Iterator to browse the logs in the map. * The order the iterator returns the logs is that of the keys. * * @return an Iterator over the elements in this map * @see com.cosylab.logging.client.cache.ILogMap */ public Iterator<ILogEntry> iterator() { return new LogIterator(this); } /** * The keys in the map * * @return The key in the map * @see com.cosylab.logging.client.cache.ILogMap */ public Set<Integer> keySet() { HashSet<Integer> ret = new HashSet<Integer>(); for (MultiFileTableRecord record: logFileTable) { ret.addAll(record.keySet()); } return ret; } /** * 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 * @see com.cosylab.logging.client.cache.ILogMap */ public int getFirstLogs(int n, Collection<Integer> keys) { int ret=0; Vector<Integer>temp = new Vector<Integer>(); for (MultiFileTableRecord record: logFileTable) { temp.clear(); ret+=record.getFirstLogs(n-ret, temp); for (int t=0; t<temp.size(); t++) { temp.set(t, temp.get(t)+record.getMinLogIdx()); } keys.addAll(temp); if (ret==n) { break; } } return ret; } /** * Return the size of the last log file used by the cache * (it is here for testing purposes) * * @see */ public long getLogFileSize() throws LogCacheException { if (logFileTable.isEmpty()) { return 0; } MultiFileTableRecord lastRecord = logFileTable.lastElement(); if (lastRecord == null) { throw new LogCacheException("Null file record"); } long fileSize; try { fileSize = lastRecord.getFileSize(); } catch (IOException ioe) { throw new LogCacheException(ioe); } return fileSize; } /** * Return the disk space used by all the files of the cache. * * @return The disk space used by all the files of the cache * @throws IOException In case of error getting the size of one of * the files */ public synchronized long getFilesSize() throws IOException { long ret=0; for (MultiFileTableRecord mftb: logFileTable) { ret+=mftb.getFileSize(); } return ret; } /** * Return the current maximum size of the log file * * @see */ public long getMaxFileSize() throws LogCacheException { return fileMaxSize; } /** * Return the current maximum size of the log file * * @see */ public void setDebugTrace(boolean flag) throws LogCacheException { debugTrace = flag; return; } /** * Return the current maximum size of the log file * * @see */ public void printFileTableInfo() throws LogCacheException { int idx; System.out.println("\nlogFileTable : total logs = "+ logsInCache); for (idx = 0; idx < logFileTable.size(); idx ++) { MultiFileTableRecord fr = logFileTable.get(idx); long actualSize; try { actualSize = fr.getFileSize(); } catch (IOException e) { throw new LogCacheException("Error getting the size of the file",e); } System.out.println("logFile "+idx+": log counter = "+ fr.getNumOfLogs()+ " Range Keys [" + fr.getMinLogIdx() + " " + fr.getMaxLogIdx()+ "] file size " + actualSize); } System.out.println("\n"); return; } /** * @return the number of the files used by the cache */ public int getNumberOfCacheFiles() { return logFileTable.size(); } }