/* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package in.srain.cube.diskcache.lru; import in.srain.cube.diskcache.CacheEntry; import in.srain.cube.diskcache.DiskCache; import in.srain.cube.diskcache.FileUtils; import in.srain.cube.set.hash.SimpleHashSet; import in.srain.cube.util.CLog; import java.io.*; import java.util.*; import java.util.concurrent.*; public final class LruActionTracer implements Runnable { final static int REDUNDANT_OP_COMPACT_THRESHOLD = 2000; static final String JOURNAL_FILE = "journal"; static final String JOURNAL_FILE_TMP = "journal.tmp"; static final String MAGIC = "lru-tracer"; static final String VERSION_1 = "1"; private static final byte ACTION_CLEAN = 1; private static final byte ACTION_DIRTY = 2; private static final byte ACTION_DELETE = 3; private static final byte ACTION_READ = 4; private static final byte ACTION_PENDING_DELETE = 5; private static final byte ACTION_FLUSH = 6; private static final String[] sACTION_LIST = new String[]{"UN_KNOW", "CLEAN", "DIRTY", "DELETE", "READ", "DELETE_PENDING", "FLUSH"}; private static final int IO_BUFFER_SIZE = 8 * 1024; private static final byte[] sPoolSync = new byte[0]; private static final int MAX_POOL_SIZE = 50; private static ActionMessage sPoolHeader; private static int sPoolSize = 0; private final LinkedHashMap<String, CacheEntry> mLruEntries = new LinkedHashMap<String, CacheEntry>(0, 0.75f, true); /** * This cache uses a single background thread to evict entries. */ private final ExecutorService mExecutorService = new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>()); private final File mJournalFile; private final File mJournalFileTmp; private boolean mIsRunning = false; private DiskCache mDiskCache; private long mSize = 0; private ConcurrentLinkedQueue<ActionMessage> mActionQueue; private File mDirectory; private long mCapacity; private int mAppVersion; private SimpleHashSet mNewCreateList; private Object mLock = new Object(); private Writer mJournalWriter; private int mRedundantOpCount; private HashMap<String, CacheEntry> mEditList; public LruActionTracer(DiskCache diskCache, File directory, int appVersion, long capacity) { mDiskCache = diskCache; mJournalFile = new File(directory, JOURNAL_FILE); mJournalFileTmp = new File(directory, JOURNAL_FILE_TMP); mDirectory = directory; mAppVersion = appVersion; mCapacity = capacity; mNewCreateList = new SimpleHashSet(); mEditList = new HashMap<String, CacheEntry>(); mActionQueue = new ConcurrentLinkedQueue<ActionMessage>(); } private static void validateKey(String key) { if (key.contains(" ") || key.contains("\n") || key.contains("\r")) { throw new IllegalArgumentException( "keys must not contain spaces or newlines: \"" + key + "\""); } } /** * try to resume last status when we got off * * @throws java.io.IOException */ public void tryToResume() throws IOException { if (mJournalFile.exists()) { try { readJournal(); processJournal(); mJournalWriter = new BufferedWriter(new FileWriter(mJournalFile, true), IO_BUFFER_SIZE); if (SimpleDiskLruCache.DEBUG) { CLog.d(SimpleDiskLruCache.LOG_TAG, "open success"); } } catch (IOException journalIsCorrupt) { journalIsCorrupt.printStackTrace(); if (SimpleDiskLruCache.DEBUG) { CLog.d(SimpleDiskLruCache.LOG_TAG, "clear old cache"); } clear(); } } else { if (SimpleDiskLruCache.DEBUG) { CLog.d(SimpleDiskLruCache.LOG_TAG, "create new cache"); } // create a new empty cache if (mDirectory.exists()) { mDirectory.delete(); } mDirectory.mkdirs(); rebuildJournal(); } } public synchronized void clear() throws IOException { // abort edit for (CacheEntry cacheEntry : new ArrayList<CacheEntry>(mLruEntries.values())) { if (cacheEntry.isUnderEdit()) { cacheEntry.abortEdit(); } } mLruEntries.clear(); mSize = 0; // delete current directory then rebuild if (SimpleDiskLruCache.DEBUG) { CLog.d(SimpleDiskLruCache.LOG_TAG, "delete directory"); } waitJobDone(); // rebuild FileUtils.deleteDirectoryQuickly(mDirectory); rebuildJournal(); } /** * Returns a {@link in.srain.cube.diskcache.CacheEntry} named {@code key}, or null if it doesn't * exist is not currently readable. If a value is returned, it is moved to * the head of the LRU queue. */ public synchronized CacheEntry getEntry(String key) throws IOException { checkNotClosed(); validateKey(key); CacheEntry cacheEntry = mLruEntries.get(key); if (cacheEntry == null) { return null; } trimToSize(); addActionLog(ACTION_READ, cacheEntry); return cacheEntry; } public synchronized CacheEntry beginEdit(String key) throws IOException { checkNotClosed(); validateKey(key); if (SimpleDiskLruCache.DEBUG) { CLog.d(SimpleDiskLruCache.LOG_TAG, "beginEdit: %s", key); } CacheEntry cacheEntry = mLruEntries.get(key); if (cacheEntry == null) { cacheEntry = new CacheEntry(mDiskCache, key); mNewCreateList.add(key); mLruEntries.put(key, cacheEntry); } mEditList.put(key, cacheEntry); addActionLog(ACTION_DIRTY, cacheEntry); return cacheEntry; } public void abortEdit(String key) { CacheEntry cacheEntry = mEditList.get(key); if (cacheEntry != null) { try { cacheEntry.abortEdit(); } catch (IOException e) { } } } public void abortEdit(CacheEntry cacheEntry) { final String cacheKey = cacheEntry.getKey(); if (SimpleDiskLruCache.DEBUG) { CLog.d(SimpleDiskLruCache.LOG_TAG, "abortEdit: %s", cacheKey); } if (mNewCreateList.contains(cacheKey)) { mLruEntries.remove(cacheKey); mNewCreateList.remove(cacheKey); } mEditList.remove(cacheKey); } public void commitEdit(CacheEntry cacheEntry) throws IOException { if (SimpleDiskLruCache.DEBUG) { CLog.d(SimpleDiskLruCache.LOG_TAG, "commitEdit: %s", cacheEntry.getKey()); } mNewCreateList.remove(cacheEntry.getKey()); mEditList.remove(cacheEntry.getKey()); mSize += cacheEntry.getSize() - cacheEntry.getLastSize(); addActionLog(ACTION_CLEAN, cacheEntry); trimToSize(); } private void readJournalLine(String line) throws IOException { String[] parts = line.split(" "); if (parts.length < 2) { throw new IOException("unexpected journal line: " + line); } if (parts.length != 3) { throw new IOException("unexpected journal line: " + line); } String key = parts[1]; if (parts[0].equals(sACTION_LIST[ACTION_DELETE])) { mLruEntries.remove(key); return; } CacheEntry cacheEntry = mLruEntries.get(key); if (cacheEntry == null) { cacheEntry = new CacheEntry(mDiskCache, key); mLruEntries.put(key, cacheEntry); } if (parts[0].equals(sACTION_LIST[ACTION_CLEAN])) { cacheEntry.setSize(Long.parseLong(parts[2])); } else if (parts[0].equals(sACTION_LIST[ACTION_DIRTY])) { // skip } else if (parts[0].equals(sACTION_LIST[ACTION_READ])) { // this work was already done by calling mLruEntries.get() } else { throw new IOException("unexpected journal line: " + line); } } /** * Computes the initial size and collects garbage as a part of opening the * cache. Dirty entries are assumed to be inconsistent and will be deleted. */ private void processJournal() throws IOException { FileUtils.deleteIfExists(mJournalFileTmp); for (Iterator<CacheEntry> i = mLruEntries.values().iterator(); i.hasNext(); ) { CacheEntry cacheEntry = i.next(); if (!cacheEntry.isUnderEdit()) { mSize += cacheEntry.getSize(); } else { cacheEntry.delete(); i.remove(); } } } /** * Creates a new journal that omits redundant information. This replaces the * current journal if it exists. */ private synchronized void rebuildJournal() throws IOException { if (mJournalWriter != null) { mJournalWriter.close(); } Writer writer = new BufferedWriter(new FileWriter(mJournalFileTmp), IO_BUFFER_SIZE); writer.write(MAGIC); writer.write("\n"); writer.write(VERSION_1); writer.write("\n"); writer.write(Integer.toString(mAppVersion)); writer.write("\n"); writer.write("\n"); for (CacheEntry cacheEntry : mLruEntries.values()) { if (cacheEntry.isUnderEdit()) { writer.write(sACTION_LIST[ACTION_DIRTY] + ' ' + cacheEntry.getKey() + " " + cacheEntry.getSize() + '\n'); } else { writer.write(sACTION_LIST[ACTION_CLEAN] + ' ' + cacheEntry.getKey() + " " + cacheEntry.getSize() + '\n'); } } writer.close(); mJournalFileTmp.renameTo(mJournalFile); mJournalWriter = new BufferedWriter(new FileWriter(mJournalFile, true), IO_BUFFER_SIZE); } private void readJournal() throws IOException { InputStream in = new BufferedInputStream(new FileInputStream(mJournalFile), IO_BUFFER_SIZE); try { String magic = FileUtils.readAsciiLine(in); String version = FileUtils.readAsciiLine(in); String appVersionString = FileUtils.readAsciiLine(in); String blank = FileUtils.readAsciiLine(in); if (!MAGIC.equals(magic) || !VERSION_1.equals(version) || !Integer.toString(mAppVersion).equals(appVersionString) || !"".equals(blank)) { throw new IOException("unexpected journal header: [" + magic + ", " + version + ", " + blank + "]"); } while (true) { try { readJournalLine(FileUtils.readAsciiLine(in)); } catch (EOFException endOfJournal) { break; } } } finally { FileUtils.closeQuietly(in); } } private void checkNotClosed() { if (mJournalFile == null) { throw new IllegalStateException("cache is closed"); } } /** * Force buffered operations to the filesystem. */ public synchronized void flush() throws IOException { checkNotClosed(); trimToSize(); addActionLog(ACTION_FLUSH, null); waitJobDone(); } private void writeActionLog(byte action, CacheEntry cacheEntry) throws IOException { mJournalWriter.write(sACTION_LIST[action] + ' ' + cacheEntry.getKey() + ' ' + cacheEntry.getSize() + '\n'); mRedundantOpCount++; if (mRedundantOpCount >= REDUNDANT_OP_COMPACT_THRESHOLD && mRedundantOpCount >= mLruEntries.size()) { mRedundantOpCount = 0; rebuildJournal(); } } private void doJob() throws IOException { synchronized (mLock) { while (!mActionQueue.isEmpty()) { ActionMessage message = mActionQueue.poll(); final CacheEntry cacheEntry = message.mCacheEntry; final byte action = message.mAction; message.recycle(); if (SimpleDiskLruCache.DEBUG) { CLog.d(SimpleDiskLruCache.LOG_TAG, "doAction: %s, key: %s", sACTION_LIST[action], cacheEntry != null ? cacheEntry.getKey() : null); } switch (action) { case ACTION_READ: writeActionLog(action, cacheEntry); break; case ACTION_DIRTY: writeActionLog(action, cacheEntry); break; case ACTION_CLEAN: writeActionLog(action, cacheEntry); break; case ACTION_DELETE: writeActionLog(action, cacheEntry); break; case ACTION_PENDING_DELETE: writeActionLog(action, cacheEntry); if (mLruEntries.containsKey(cacheEntry.getKey())) { continue; } cacheEntry.delete(); break; case ACTION_FLUSH: mJournalWriter.flush(); break; } } mLock.notify(); } } private void waitJobDone() { if (SimpleDiskLruCache.DEBUG) { CLog.d(SimpleDiskLruCache.LOG_TAG, "waitJobDone"); } synchronized (mLock) { if (mIsRunning) { while (!mActionQueue.isEmpty()) { try { mLock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } if (SimpleDiskLruCache.DEBUG) { CLog.d(SimpleDiskLruCache.LOG_TAG, "job is done"); } } private void addActionLog(byte action, CacheEntry cacheEntry) { mActionQueue.add(ActionMessage.obtain(action, cacheEntry)); if (!mIsRunning) { mIsRunning = true; mExecutorService.submit(this); } } public synchronized void close() throws IOException { if (isClosed()) { return; // already closed } for (CacheEntry cacheEntry : new ArrayList<CacheEntry>(mLruEntries.values())) { if (cacheEntry.isUnderEdit()) { cacheEntry.abortEdit(); } } trimToSize(); waitJobDone(); rebuildJournal(); mJournalWriter.close(); mJournalWriter = null; } private boolean isClosed() { return mJournalWriter == null; } @Override public void run() { try { doJob(); } catch (IOException e) { e.printStackTrace(); } mIsRunning = false; } /** * remove files from list, delete files * * @throws java.io.IOException */ private void trimToSize() throws IOException { synchronized (this) { if (mSize > mCapacity) { if (SimpleDiskLruCache.DEBUG) { CLog.d(SimpleDiskLruCache.LOG_TAG, "should trim, current is: %s", mSize); } } while (mSize > mCapacity) { Map.Entry<String, CacheEntry> toEvict = mLruEntries.entrySet().iterator().next(); String key = toEvict.getKey(); CacheEntry cacheEntry = toEvict.getValue(); mLruEntries.remove(key); mSize -= cacheEntry.getSize(); addActionLog(ACTION_PENDING_DELETE, cacheEntry); if (SimpleDiskLruCache.DEBUG) { CLog.d(SimpleDiskLruCache.LOG_TAG, "pending remove: %s, size: %s, after remove total: %s", key, cacheEntry.getSize(), mSize); } } } } public synchronized boolean delete(String key) throws IOException { if (SimpleDiskLruCache.DEBUG) { CLog.d(SimpleDiskLruCache.LOG_TAG, "delete: %s", key); } checkNotClosed(); validateKey(key); CacheEntry cacheEntry = mLruEntries.get(key); if (cacheEntry == null) { return false; } // delete at once cacheEntry.delete(); mSize -= cacheEntry.getSize(); cacheEntry.setSize(0); mLruEntries.remove(key); addActionLog(ACTION_DELETE, cacheEntry); return true; } public long getSize() { return mSize; } public long getCapacity() { return mCapacity; } public File getDirectory() { return mDirectory; } public boolean has(String key) { return mLruEntries.containsKey(key) && !mNewCreateList.contains(key); } private static class ActionMessage { private byte mAction; private CacheEntry mCacheEntry; private ActionMessage mNext; public ActionMessage(byte action, CacheEntry cacheEntry) { mAction = action; mCacheEntry = cacheEntry; } public static ActionMessage obtain(byte action, CacheEntry cacheEntry) { synchronized (sPoolSync) { if (sPoolHeader != null) { ActionMessage m = sPoolHeader; sPoolHeader = m.mNext; m.mNext = null; sPoolSize--; m.mAction = action; m.mCacheEntry = cacheEntry; return m; } } return new ActionMessage(action, cacheEntry); } public void recycle() { mAction = 0; mCacheEntry = null; synchronized (sPoolSync) { if (sPoolSize < MAX_POOL_SIZE) { mNext = sPoolHeader; sPoolHeader = this; sPoolSize++; } } } } }