/** * */ package com.trendrr.oss.appender; import java.io.File; import java.util.Date; import java.util.Map; import java.util.Timer; import java.util.TimerTask; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.cache.RemovalListener; import com.google.common.cache.RemovalNotification; import com.trendrr.oss.StringHelper; import com.trendrr.oss.TimeAmount; import com.trendrr.oss.Timeframe; import com.trendrr.oss.appender.exceptions.FileClosedException; import com.trendrr.oss.exceptions.TrendrrIOException; /** * This is a slightly different implementation then RollingFileAppender. * * It is not meant to keep a rolling number of files available, but instead * is meant to write to time based files, then it calls a callback once those files are * considered stale (have not been written to in 5 minutes). * * This is useful for backing up event based items into a secondary storage where items need to be grouped * by time, but they might not arrive in cronological order. * * methodology: * * 1. files get uploaded if they have not been written to for five minutes * 2. files have a maxbytes param, will upload once that file size is achieved. * 3. filename in the form {epoch}_{timeamount}__{randomstring}(.gz) * * Note the gzip appendar seems to be slightly touchy. * * @author Dustin Norlander * @created Jun 17, 2013 * */ public class TimeAmountFileAppender { protected static Log log = LogFactory.getLog(TimeAmountFileAppender.class); protected TimeAmount amount; protected TimeAmount staleFileCheck; protected LoadingCache<Long, TimeAmountFile> cache = null; protected TimeAmountFileCallback callback; protected long maxBytes; protected String directory; protected boolean gzip = false; protected Timer cleaner = null; public TimeAmountFileAppender(TimeAmountFileCallback callback, TimeAmount amount, TimeAmount staleFileCheck, String dir, long maxBytes) { this(callback, amount, staleFileCheck, dir, maxBytes, false); } public TimeAmountFileAppender(TimeAmountFileCallback callback, TimeAmount amount, TimeAmount staleFileCheck, String dir, long maxBytes, boolean gzip) { this.amount = amount; this.callback = callback; this.directory = dir; this.maxBytes = maxBytes; this.gzip = gzip; this.staleFileCheck = staleFileCheck; } /** * initializes the appender. this can only be called once, and is automatically called during the * first append operation. */ public synchronized void init() { if (this.cache != null) { return; } log.warn("Creating expiration cache " + staleFileCheck.getAmount() + " " + staleFileCheck.getTimeframe().getTimeUnit()); this.cache = CacheBuilder.newBuilder() .expireAfterAccess(staleFileCheck.getAmount(), staleFileCheck.getTimeframe().getTimeUnit()) .removalListener( new RemovalListener<Long, TimeAmountFile>() { @Override public void onRemoval(RemovalNotification<Long, TimeAmountFile> rn) { log.warn("onRemoval! " + rn.getKey() + " " + rn.getValue()); staleFile(rn.getValue()); } }) .build( new CacheLoader<Long, TimeAmountFile>() { public TimeAmountFile load(Long epoch) throws Exception { System.out.println("LOADIGN EPOCH: " + epoch); return newFile(epoch); } }); //now load any files already in the directory. File f[] = new File(this.directory).listFiles(); if (f != null) { for (File file : f) { try { TimeAmountFile taf = new TimeAmountFile(file, maxBytes); log.warn("Adding file : " + file.getAbsolutePath() + " to appender "); //make sure we dont upload both a gz and non-gz file with the same name if (file.getName().endsWith(".gz")) { if (new File(StringHelper.trim(file.getAbsolutePath(), ".gz")).exists()) { log.warn("Deleting gz file, keeping original. " + file.getAbsolutePath()); file.delete(); continue; } else { //need to upload it since we cant append to a gz file staleFile(taf); continue; } } //stale all the files in the directory. // we dont know if the previous shutdown was safe, so // we dont want to continue writing to the file in case // a partial json packet was written or something.. staleFile(taf); // if (taf.getEpoch() != currentEpoch) { // staleFile(taf); // continue; // } // this.cache.put(taf.getEpoch(), taf); } catch (Exception x) { log.error("Caught", x); } } } //set up a timer to periodically clean out the cache. // does this every 5 minutes. // TODO: set this for the staleFileCheck timeamount this.cleaner = new Timer(true); this.cleaner.schedule(new TimerTask() { @Override public void run() { log.warn("Cache cleanup timer for " + directory); cache.cleanUp(); } }, 5 * ((int)(Math.random() * 60*1000)), 5*60*1000); } public String toString() { StringBuilder str = new StringBuilder(); str.append("*** Timeamount file appender * "); Map<Long, TimeAmountFile> mp = this.cache.asMap(); for (Long epoch : mp.keySet()) { str.append(epoch); str.append(","); } str.append(" ** /n"); return str.toString(); } public void staleFile(TimeAmountFile f) { f.stale(callback); } protected TimeAmountFile newFile(Long epoch) throws Exception { return new TimeAmountFile(this.amount, epoch, this.directory, this.maxBytes, this.gzip); } public void appendLine(Date date, String str) throws Exception { this.append(date, str + "\n"); } public synchronized void append(Date date, String str) throws Exception { if (this.cache == null) { this.init(); } long epoch = this.amount.toTrendrrEpoch(date).longValue(); //Try to get the file max 5 times. for (int i=0; i < 5; i++) { TimeAmountFile file = null; try { file = this.cache.get(epoch); file.append(str); return; } catch (FileClosedException x) { this.staleFile(file); this.cache.invalidate(epoch); //try again.. } } throw new TrendrrIOException("Unable to get file for epoch: " + epoch); } }