org.aspectj.weaver.tools.cache.AsynchronousFileCacheBacking Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of aspectjtools Show documentation
Show all versions of aspectjtools Show documentation
Tools from the AspectJ project
/*******************************************************************************
* Copyright (c) 2012 Contributors.
* All rights reserved.
* This program and the accompanying materials are made available
* under the terms of the Eclipse Public License v 2.0
* which accompanies this distribution and is available at
* https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.txt
*
* Contributors:
* Lyor Goldstein (vmware) add support for weaved class being re-defined
*******************************************************************************/
package org.aspectj.weaver.tools.cache;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import org.aspectj.util.FileUtil;
import org.aspectj.util.LangUtil;
import org.aspectj.weaver.tools.Trace;
import org.aspectj.weaver.tools.TraceFactory;
/**
* Uses a background thread to do the actual I/O and for caching "persistence"
* so that the caching works faster on repeated activations of the application.
* The class maintains an in-memory cache, and uses a queue of {@link AsyncCommand}s
* to signal to a background thread various actions required to "synchronize"
* the in-memory cache with the persisted copy. Whenever there is a cache miss
* from the {@link #get(CachedClassReference, byte[])} call, the weaver issues a
* {@link #put(CachedClassEntry, byte[])} call. This call has 2 side-effects:
*
* -
* The in-memory cache is updated so that subsequent calls to {@link #get(CachedClassReference, byte[])}
* will not return the mapped value.
*
*
* -
* An "update index" {@link AsyncCommand} is posted to the background
* thread so that the newly mapped value will be persisted (eventually)
*
*
* The actual persistence is implemented by the concrete classes
*/
public abstract class AsynchronousFileCacheBacking extends AbstractIndexedFileCacheBacking {
private static final BlockingQueue commandsQ= new LinkedBlockingQueue<>();
private static final ExecutorService execService=Executors.newSingleThreadExecutor();
private static Future> commandsRunner;
protected final Map index, exposedIndex;
protected final Map bytesMap, exposedBytes;
protected AsynchronousFileCacheBacking (File cacheDir) {
super(cacheDir);
index = readIndex(cacheDir, getIndexFile());
exposedIndex = Collections.unmodifiableMap(index);
bytesMap = readClassBytes(index, cacheDir);
exposedBytes = Collections.unmodifiableMap(bytesMap);
}
@Override
protected Map getIndex() {
return index;
}
public CachedClassEntry get(CachedClassReference ref, byte[] originalBytes) {
String key=ref.getKey();
final IndexEntry indexEntry;
synchronized(index) {
if ((indexEntry=index.get(key)) == null) {
return null;
}
}
if (crc(originalBytes) != indexEntry.crcClass) {
if ((logger != null) && logger.isTraceEnabled()) {
logger.debug("get(" + getCacheDirectory() + ") mismatched original class bytes CRC for " + key);
}
remove(key);
return null;
}
if (indexEntry.ignored) {
return new CachedClassEntry(ref, WeavedClassCache.ZERO_BYTES, CachedClassEntry.EntryType.IGNORED);
}
final byte[] bytes;
synchronized(bytesMap) {
/*
* NOTE: we assume that keys represent classes so if we have their
* bytes they will not be re-created
*/
if ((bytes=bytesMap.remove(key)) == null) {
return null;
}
}
if (indexEntry.generated) {
return new CachedClassEntry(ref, bytes, CachedClassEntry.EntryType.GENERATED);
} else {
return new CachedClassEntry(ref, bytes, CachedClassEntry.EntryType.WEAVED);
}
}
public void put(CachedClassEntry entry, byte[] originalBytes) {
String key=entry.getKey();
byte[] bytes=entry.isIgnored() ? null : entry.getBytes();
synchronized(index) {
IndexEntry indexEntry=index.get(key);
if (indexEntry != null) {
return;
}
/*
* Note: we do not cache the class bytes - only send them to
* be saved. The assumption is that the 'put' call was invoked
* because 'get' failed to return any bytes. And since we assume
* that each class bytes are required only once, there is no
* need to cache them
*/
indexEntry = createIndexEntry(entry, originalBytes);
index.put(key, indexEntry);
}
if (!postCacheCommand(new InsertCommand(this, key, bytes))) {
if ((logger != null) && logger.isTraceEnabled()) {
logger.error("put(" + getCacheDirectory() + ") Failed to post insert command for " + key);
}
}
if ((logger != null) && logger.isTraceEnabled()) {
logger.debug("put(" + getCacheDirectory() + ")[" + key + "] inserted");
}
}
public void remove(CachedClassReference ref) {
remove(ref.getKey());
}
protected IndexEntry remove (String key) {
IndexEntry entry;
synchronized(index) {
entry = index.remove(key);
}
synchronized(bytesMap) {
bytesMap.remove(key);
}
if (!postCacheCommand(new RemoveCommand(this, key))) {
if ((logger != null) && logger.isTraceEnabled()) {
logger.error("remove(" + getCacheDirectory() + ") Failed to post remove command for " + key);
}
}
if (entry != null) {
if (!key.equals(entry.key)) {
if ((logger != null) && logger.isTraceEnabled()) {
logger.error("remove(" + getCacheDirectory() + ") Mismatched keys: " + key + " / " + entry.key);
}
} else if ((logger != null) && logger.isTraceEnabled()) {
logger.debug("remove(" + getCacheDirectory() + ")[" + key + "] removed");
}
}
return entry;
}
public List getIndexEntries () {
synchronized(index) {
if (index.isEmpty()) {
return Collections.emptyList();
} else {
return new ArrayList<>(index.values());
}
}
}
public Map getIndexMap () {
return exposedIndex;
}
public Map getBytesMap () {
return exposedBytes;
}
public void clear() {
synchronized(index) {
index.clear();
}
if (!postCacheCommand(new ClearCommand(this))) {
if ((logger != null) && logger.isTraceEnabled()) {
logger.error("Failed to post clear command for " + getIndexFile());
}
}
}
protected void executeCommand (AsyncCommand cmd) throws Exception {
if (cmd instanceof ClearCommand) {
executeClearCommand();
} else if (cmd instanceof UpdateIndexCommand) {
executeUpdateIndexCommand();
} else if (cmd instanceof InsertCommand) {
executeInsertCommand((InsertCommand) cmd);
} else if (cmd instanceof RemoveCommand) {
executeRemoveCommand((RemoveCommand) cmd);
} else {
throw new UnsupportedOperationException("Unknown command: " + cmd);
}
}
protected void executeClearCommand () throws Exception {
FileUtil.deleteContents(getIndexFile());
FileUtil.deleteContents(getCacheDirectory());
}
protected void executeUpdateIndexCommand () throws Exception {
writeIndex(getIndexFile(), getIndexEntries());
}
protected void executeInsertCommand (InsertCommand cmd) throws Exception {
writeIndex(getIndexFile(), getIndexEntries());
byte[] bytes=cmd.getClassBytes();
if (bytes != null) {
writeClassBytes(cmd.getKey(), bytes);
}
}
protected void executeRemoveCommand (RemoveCommand cmd) throws Exception {
Exception err=null;
try {
removeClassBytes(cmd.getKey());
} catch(Exception e) {
err = e;
}
writeIndex(getIndexFile(), getIndexEntries());
if (err != null) {
throw err; // check if the class bytes remove had any problems
}
}
/**
* Helper for {@link #executeRemoveCommand(RemoveCommand)}
* @param key The key representing the class whose bytes are to be removed
* @throws Exception if failed to remove class bytes
*/
protected abstract void removeClassBytes (String key) throws Exception;
protected abstract Map readClassBytes (Map indexMap, File cacheDir);
@Override
public String toString() {
return getClass().getSimpleName() + "[" + String.valueOf(getCacheDirectory()) + "]";
}
protected static final T createBacking (
File cacheDir, AsynchronousFileCacheBackingCreator creator) {
final Trace trace=TraceFactory.getTraceFactory().getTrace(AsynchronousFileCacheBacking.class);
if (!cacheDir.exists()) {
if (!cacheDir.mkdirs()) {
if ((trace != null) && trace.isTraceEnabled()) {
trace.error("Unable to create cache directory at " + cacheDir.getAbsolutePath());
}
return null;
}
}
if (!cacheDir.canWrite()) {
if ((trace != null) && trace.isTraceEnabled()) {
trace.error("Cache directory is not writable at " + cacheDir.getAbsolutePath());
}
return null;
}
// start the service (if needed) only if successfully create the backing instance
T backing=creator.create(cacheDir);
synchronized(execService) {
if (commandsRunner == null) {
commandsRunner = execService.submit(new Runnable() {
@SuppressWarnings("synthetic-access")
public void run() {
for ( ; ; ) {
try {
AsyncCommand cmd=commandsQ.take();
try {
AsynchronousFileCacheBacking cache=cmd.getCache();
cache.executeCommand(cmd);
} catch(Exception e) {
if ((trace != null) && trace.isTraceEnabled()) {
trace.error("Failed (" + e.getClass().getSimpleName() + ")"
+ " to execute " + cmd + ": " + e.getMessage(), e);
}
}
} catch(InterruptedException e) {
if ((trace != null) && trace.isTraceEnabled()) {
trace.warn("Interrupted");
}
Thread.currentThread().interrupt();
break;
}
}
}
});
}
}
// fire-up an update-index command in case index was changed by the constructor
if (!postCacheCommand(new UpdateIndexCommand(backing))) {
if ((trace != null) && trace.isTraceEnabled()) {
trace.warn("Failed to offer update index command to " + cacheDir.getAbsolutePath());
}
}
return backing;
}
public static final boolean postCacheCommand (AsyncCommand cmd) {
return commandsQ.offer(cmd);
}
public interface AsynchronousFileCacheBackingCreator {
T create (File cacheDir);
}
/**
* Represents an asynchronous command that can be sent to the
* {@link AsynchronousFileCacheBacking} instance to be executed
* on it asynchronously
*/
public interface AsyncCommand {
/**
* @return The {@link AsynchronousFileCacheBacking} on which
* this command is supposed to be executed
* @see AsynchronousFileCacheBacking#executeCommand(AsyncCommand)
*/
AsynchronousFileCacheBacking getCache ();
}
public static abstract class AbstractCommand implements AsyncCommand {
private final AsynchronousFileCacheBacking cache;
protected AbstractCommand (AsynchronousFileCacheBacking backing) {
if ((cache=backing) == null) {
throw new IllegalStateException("No backing cache specified");
}
}
public final AsynchronousFileCacheBacking getCache () {
return cache;
}
@Override
public String toString() {
return getClass().getSimpleName() + "[" + getCache() + "]";
}
}
public static class ClearCommand extends AbstractCommand {
public ClearCommand (AsynchronousFileCacheBacking cache) {
super(cache);
}
}
public static class UpdateIndexCommand extends AbstractCommand {
public UpdateIndexCommand (AsynchronousFileCacheBacking cache) {
super(cache);
}
}
/**
* Base class for {@link AbstractCommand}s that refer to a cache key
*/
public static abstract class KeyedCommand extends AbstractCommand {
private final String key;
protected KeyedCommand (AsynchronousFileCacheBacking cache, String keyValue) {
super(cache);
if (LangUtil.isEmpty(keyValue)) {
throw new IllegalStateException("No key value");
}
key = keyValue;
}
public final String getKey () {
return key;
}
@Override
public String toString() {
return super.toString() + "[" + getKey() + "]";
}
}
public static class RemoveCommand extends KeyedCommand {
public RemoveCommand (AsynchronousFileCacheBacking cache, String keyValue) {
super(cache, keyValue);
}
}
public static class InsertCommand extends KeyedCommand {
private final byte[] bytes;
public InsertCommand (AsynchronousFileCacheBacking cache, String keyValue, byte[] classBytes) {
super(cache, keyValue);
bytes = classBytes;
}
public final byte[] getClassBytes () {
return bytes;
}
}
}