Author: hibou
Date: Fri Jul 30 09:14:11 2010
New Revision: 980696
URL: http://svn.apache.org/viewvc?rev=980696&view=rev
Log:
Add an experimental (and maybe overkill) groovy script cache to improve startup
time
Added:
ant/sandbox/groovyfront/src/main/java/org/apache/ant/groovyfront/cache/
ant/sandbox/groovyfront/src/main/java/org/apache/ant/groovyfront/cache/CachedGroovyScriptLoader.java
(with props)
ant/sandbox/groovyfront/src/main/java/org/apache/ant/groovyfront/cache/GroovyScriptCacheCleaner.java
(with props)
ant/sandbox/groovyfront/src/main/java/org/apache/ant/groovyfront/cache/GroovyScriptLoader.java
(with props)
ant/sandbox/groovyfront/src/main/java/org/apache/ant/groovyfront/cache/ThreadSafeFileLocker.java
(with props)
ant/sandbox/groovyfront/src/test/java/org/apache/ant/groovyfront/cache/
ant/sandbox/groovyfront/src/test/java/org/apache/ant/groovyfront/cache/GroovyScriptCacheTest.java
(with props)
Modified:
ant/sandbox/groovyfront/src/main/java/org/apache/ant/groovyfront/GroovyFrontProjectHelper.java
Modified:
ant/sandbox/groovyfront/src/main/java/org/apache/ant/groovyfront/GroovyFrontProjectHelper.java
URL:
http://svn.apache.org/viewvc/ant/sandbox/groovyfront/src/main/java/org/apache/ant/groovyfront/GroovyFrontProjectHelper.java?rev=980696&r1=980695&r2=980696&view=diff
==============================================================================
---
ant/sandbox/groovyfront/src/main/java/org/apache/ant/groovyfront/GroovyFrontProjectHelper.java
(original)
+++
ant/sandbox/groovyfront/src/main/java/org/apache/ant/groovyfront/GroovyFrontProjectHelper.java
Fri Jul 30 09:14:11 2010
@@ -18,24 +18,20 @@
package org.apache.ant.groovyfront;
import groovy.lang.Binding;
-import groovy.lang.GroovyShell;
import groovy.lang.Script;
import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Vector;
+import org.apache.ant.groovyfront.cache.CachedGroovyScriptLoader;
+import org.apache.ant.groovyfront.cache.GroovyScriptCacheCleaner;
+import org.apache.ant.groovyfront.cache.GroovyScriptLoader;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.ProjectHelper;
import org.apache.tools.ant.types.Resource;
-import org.apache.tools.ant.util.FileUtils;
import org.codehaus.groovy.control.CompilationFailedException;
public class GroovyFrontProjectHelper extends ProjectHelper {
@@ -44,6 +40,48 @@ public class GroovyFrontProjectHelper ex
private static final String REFID_BUILDER = "groovyfront.builder";
+ private static final String SYSPROP_USECACHE = "ant.groovyfront.usecache";
+
+ private static final String SYSPROP_CLEANER_MINPERIOD =
"ant.groovyfront.cachecleaner.minperiod";
+
+ private static final String SYSPROP_CLEANER_TIMETOLIVE =
"ant.groovyfront.cachecleaner.timetolive";
+
+ private static final String SYSPROP_CLEANER_MAXTOKEEP =
"ant.groovyfront.cachecleaner.maxtokeep";
+
+ private static final String SYSPROP_CACHEDIR = "ant.groovyfront.cachedir";
+
+ private static final String USER_HOMEDIR = "user.home";
+ private static final String ANT_PRIVATEDIR = ".ant";
+ private static final String GROOVYFRONT_CACHE = "groovyfront-cache";
+
+ private boolean useCache = Boolean.parseBoolean(System
+ .getProperty(SYSPROP_USECACHE));
+
+ private long cleanerMinPeriod;
+
+ private Long cleanerTimetolive;
+
+ private Integer cleanerMaxtokeep;
+
+ public GroovyFrontProjectHelper() {
+ String cleanerMinPeriodValue = System
+ .getProperty(SYSPROP_CLEANER_MINPERIOD);
+ if (cleanerMinPeriodValue == null) {
+ // by default clean no more than every hour
+ cleanerMinPeriod = 1000 * 60 * 60;
+ } else {
+ cleanerMinPeriod = Long.parseLong(cleanerMinPeriodValue);
+ }
+ String timetoliveValue =
System.getProperty(SYSPROP_CLEANER_TIMETOLIVE);
+ if (timetoliveValue != null) {
+ cleanerTimetolive = new Long(timetoliveValue);
+ }
+ String maxtokeepValue = System.getProperty(SYSPROP_CLEANER_MAXTOKEEP);
+ if (maxtokeepValue != null) {
+ cleanerMaxtokeep = new Integer(maxtokeepValue);
+ }
+ }
+
public String getDefaultBuildFile() {
return "build.groovy";
}
@@ -53,23 +91,31 @@ public class GroovyFrontProjectHelper ex
}
public void parse(Project project, Object source) throws BuildException {
+ if (!(source instanceof Resource)) {
+ throw new BuildException(
+ "The GroovyFrontProjectHelper is expecting a Resource as
the source of the build file. Got "
+ + source.getClass().getName() + " instead.");
+ }
+ Resource resource = (Resource) source;
Vector/* <Object> */stack = getImportStack();
stack.addElement(source);
GroovyFrontParsingContext context = null;
- context = (GroovyFrontParsingContext)
project.getReference(REFID_CONTEXT);
+ context = (GroovyFrontParsingContext) project
+ .getReference(REFID_CONTEXT);
if (context == null) {
context = new GroovyFrontParsingContext();
project.addReference(REFID_CONTEXT, context);
}
if (getImportStack().size() > 1) {
- Map/* <String, Target> */currentTargets =
context.getCurrentTargets();
+ Map/* <String, Target> */currentTargets = context
+ .getCurrentTargets();
String currentProjectName = context.getCurrentProjectName();
boolean imported = context.isImported();
try {
context.setImported(true);
context.setCurrentTargets(new HashMap/* <String, Target> */());
- parse(project, source, context);
+ parse(project, resource, context);
} finally {
context.setCurrentTargets(currentTargets);
context.setCurrentProjectName(currentProjectName);
@@ -78,67 +124,62 @@ public class GroovyFrontProjectHelper ex
} else {
// top level file
context.setCurrentTargets(new HashMap/* <String, Target> */());
- parse(project, source, context);
+ parse(project, resource, context);
}
}
- private void parse(Project project, Object source,
GroovyFrontParsingContext context) throws BuildException {
- InputStream in;
- String buildFileName = null;
-
- try {
- if (source instanceof File) {
- File buildFile = (File) source;
- buildFileName = buildFile.toString();
- buildFile =
FileUtils.getFileUtils().normalize(buildFile.getAbsolutePath());
- context.setBuildFile(buildFile);
- in = new FileInputStream(buildFile);
- // } else if (source instanceof InputStream ) {
- } else if (source instanceof URL) {
- URL url = (URL) source;
- buildFileName = url.toString();
- in = url.openStream();
- // } else if (source instanceof InputSource ) {
- } else if (source instanceof Resource) {
- buildFileName = ((Resource) source).getName();
- in = ((Resource) source).getInputStream();
- } else {
- throw new BuildException("Source " +
source.getClass().getName() + " not supported by this plugin");
- }
- } catch (IOException e) {
- throw new BuildException("Error reading groovy file " +
buildFileName + ": " + e.getMessage(), e);
- }
+ private void parse(Project project, Resource resource,
+ GroovyFrontParsingContext context) throws BuildException {
+ String buildFileName = resource.getName();
// set explicitly before starting ?
if (project.getProperty("basedir") != null) {
project.setBasedir(project.getProperty("basedir"));
- // NB: this won't be overridden as it is a user property (see
GroovyFrontProject class)
+ // NB: this won't be overridden as it is a user property (see
+ // GroovyFrontProject class)
} else {
- // set the property even if it may be overridden within the groovy
file
+ // set the property even if it may be overridden within the groovy
+ // file
project.setBasedir(context.getBuildFileParent().getAbsolutePath());
}
- // wrap the project instance so we can be in control of the set on the
properties on the project
+ // wrap the project instance so we can be in control of the set on the
+ // properties on the project
GroovyFrontProject groovyFrontProject;
if (project instanceof GroovyFrontProject) {
groovyFrontProject = (GroovyFrontProject) project;
} else {
- groovyFrontProject = new GroovyFrontProject(project, context,
buildFileName);
+ groovyFrontProject = new GroovyFrontProject(project, context,
+ buildFileName);
}
- GroovyFrontBuilder antBuilder = new
GroovyFrontBuilder(groovyFrontProject);
+ GroovyFrontBuilder antBuilder = new GroovyFrontBuilder(
+ groovyFrontProject);
groovyFrontProject.addReference(REFID_BUILDER, antBuilder);
Binding binding = new GroovyFrontBinding(groovyFrontProject,
antBuilder);
- GroovyShell groovyShell = new GroovyShell(getClass().getClassLoader(),
binding);
+
+ GroovyScriptLoader scriptLoader;
+ if (useCache) {
+ File cacheDir = getCacheDir();
+ scriptLoader = new CachedGroovyScriptLoader(cacheDir);
+ GroovyScriptCacheCleaner.launchClean(groovyFrontProject, cacheDir,
+ cleanerMinPeriod, cleanerTimetolive, cleanerMaxtokeep);
+ } else {
+ scriptLoader = new GroovyScriptLoader();
+ }
+
final Script script;
try {
- script = groovyShell.parse(new InputStreamReader(in),
asGroovyClass(buildFileName));
+ script = scriptLoader.loadScript(resource, binding, this.getClass()
+ .getClassLoader());
} catch (CompilationFailedException e) {
- throw new BuildException("Error reading groovy file " +
buildFileName + ": " + e.getMessage(), e);
+ throw new BuildException("Error reading groovy file "
+ + buildFileName + ": " + e.getMessage(), e);
}
+
script.setBinding(binding);
- script.setMetaClass(new
GroovyFrontScriptMetaClass(script.getMetaClass(), groovyFrontProject,
antBuilder,
- context));
+ script.setMetaClass(new GroovyFrontScriptMetaClass(script
+ .getMetaClass(), groovyFrontProject, antBuilder, context));
new GroovyRunner() {
protected void doRun() {
script.run();
@@ -146,7 +187,17 @@ public class GroovyFrontProjectHelper ex
}.run();
}
- private String asGroovyClass(String filename) {
- return filename.replaceAll("-", "_");
+ private File getCacheDir() {
+ String cacheDirPath = System.getProperty(SYSPROP_CACHEDIR);
+ File dir;
+ if (cacheDirPath != null) {
+ dir = new File(cacheDirPath);
+ } else {
+ String userHome = System.getProperty(USER_HOMEDIR);
+ dir = new File(userHome + File.separatorChar + ANT_PRIVATEDIR
+ + File.separatorChar + GROOVYFRONT_CACHE);
+ }
+ return dir;
}
+
}
Added:
ant/sandbox/groovyfront/src/main/java/org/apache/ant/groovyfront/cache/CachedGroovyScriptLoader.java
URL:
http://svn.apache.org/viewvc/ant/sandbox/groovyfront/src/main/java/org/apache/ant/groovyfront/cache/CachedGroovyScriptLoader.java?rev=980696&view=auto
==============================================================================
---
ant/sandbox/groovyfront/src/main/java/org/apache/ant/groovyfront/cache/CachedGroovyScriptLoader.java
(added)
+++
ant/sandbox/groovyfront/src/main/java/org/apache/ant/groovyfront/cache/CachedGroovyScriptLoader.java
Fri Jul 30 09:14:11 2010
@@ -0,0 +1,353 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.ant.groovyfront.cache;
+
+import groovy.lang.Binding;
+import groovy.lang.Script;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.channels.FileLock;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+import org.apache.tools.ant.BuildException;
+import org.apache.tools.ant.types.Resource;
+import org.apache.tools.ant.util.FileUtils;
+import org.codehaus.groovy.runtime.InvokerHelper;
+
+public class CachedGroovyScriptLoader extends GroovyScriptLoader {
+
+ // for the hex encoder
+ private static final char[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5',
+ '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
+
+ static final String TIMESTAMP_FILE = ".timestamp";
+
+ private File cacheDir;
+ private MessageDigest md5Digester;
+
+ public CachedGroovyScriptLoader(File cacheDir) {
+ initDigester();
+ this.cacheDir = cacheDir;
+ checkCacheDir();
+ }
+
+ private void initDigester() {
+ try {
+ md5Digester = MessageDigest.getInstance("MD5");
+ } catch (NoSuchAlgorithmException e) {
+ throw new BuildException(
+ "The MD5 hash algorithm is needed in the jvm to use the
groovyfront cache",
+ e);
+ }
+ }
+
+ private void checkCacheDir() {
+ if (cacheDir.exists()) {
+ if (!cacheDir.isDirectory()) {
+ throw new BuildException(
+ "The groovyfront cache is not a directory: "
+ + cacheDir.getAbsolutePath());
+ }
+ } else {
+ boolean created = cacheDir.mkdirs();
+ if (!created) {
+ throw new BuildException(
+ "The groovyfront cache could not be created at: "
+ + cacheDir.getAbsolutePath());
+ }
+ }
+ }
+
+ public Script loadScript(Resource r, Binding binding, ClassLoader parent) {
+ long timestamp = r.getLastModified();
+ if (timestamp == Resource.UNKNOWN_DATETIME) {
+ // we cannot cache resource which don't have a modification time
+ // if is especially true for dynamically build resources
+ return parseScript(r, binding, parent, null);
+ }
+
+ // try to get a unique identifier from a resource
+ // name + implementation + hash code
+ byte[] md5 = md5Digester
+ .digest((r.getName() + r.getClass().getName() + r.hashCode())
+ .getBytes());
+ String encodedMD5 = encodeHex(md5);
+
+ File scriptCache = new File(cacheDir, encodedMD5);
+ if (scriptCache.exists()) {
+ return checkAndLoadFromCache(r, timestamp, binding, parent,
+ scriptCache);
+ }
+
+ File tmpScriptCache = new File(cacheDir, encodedMD5 + ".tmp");
+ FileLock fileLock = creatAndLockTmpCache(r, tmpScriptCache);
+
+ if (scriptCache.exists()) {
+ // the cache has been created by another thread or another jvm
+ return checkAndLoadFromCache(r, timestamp, binding, parent,
+ scriptCache);
+ }
+
+ try {
+ // do build the cache
+ Script script = buildCache(r, binding, parent, tmpScriptCache);
+
+ // promote the build as the real cache
+ // hopefully this will get atomic
+ // TODO check if on Windows we can move a locked folder
+ tmpScriptCache.renameTo(scriptCache);
+
+ return script;
+ } finally {
+ ThreadSafeFileLocker.releaseDir(tmpScriptCache, fileLock);
+ }
+ }
+
+ private Script checkAndLoadFromCache(Resource r, long timestamp,
+ Binding binding, ClassLoader parent, File scriptCache) {
+ if (!scriptCache.isDirectory()) {
+ throw new BuildException(
+ "The groovyfront cache for the build file '" + r
+ + "' is not a directory: '"
+ + scriptCache.getAbsolutePath()
+ + "'. You should delete that file and retry.");
+ }
+
+ // ensure that only one thread is reading that particular cache entry
+ FileLock fileLock;
+ try {
+ fileLock = ThreadSafeFileLocker.lockDir(scriptCache);
+ } catch (FileNotFoundException e) {
+ throw new BuildException(
+ "A folder has been deleted while tryin to access it: "
+ + scriptCache.getAbsolutePath());
+ } catch (IOException e) {
+ throw new BuildException(
+ "A lock could not be obtained on the folder "
+ + scriptCache.getAbsolutePath());
+ }
+
+ try {
+ long cachedTimestamp = readTimestamp(r, scriptCache);
+ if (cachedTimestamp == timestamp) {
+ return loadFromCache(r, scriptCache, binding, parent);
+ }
+
+ // out dated cache, remove old stuff and recreate it
+ try {
+ cleanCacheDir(scriptCache);
+ } catch (IOException e) {
+ throw new BuildException(e.getMessage());
+ }
+
+ return buildCache(r, binding, parent, scriptCache);
+ } finally {
+ ThreadSafeFileLocker.releaseDir(scriptCache, fileLock);
+ }
+ }
+
+ static void cleanCacheDir(File scriptCache) throws IOException {
+ File[] files = scriptCache.listFiles();
+ for (int i = 0; i < files.length; i++) {
+ if (files[i].isDirectory()) {
+ cleanCacheDir(files[i]);
+ files[i].delete();
+ } else if (!FileUtils.getFileUtils().tryHardToDelete(files[i])) {
+ throw new IOException("Unable to delete old cached file "
+ + files[i].getAbsolutePath());
+ }
+ }
+ }
+
+ private FileLock creatAndLockTmpCache(Resource r, File tmpScriptCache) {
+ boolean created = tmpScriptCache.mkdirs();
+ if (!created && !tmpScriptCache.exists()) {
+ throw new BuildException(
+ "The groovyfront cache for the build file '" + r
+ + "' could not be created at: "
+ + tmpScriptCache.getAbsolutePath());
+ }
+ try {
+ return ThreadSafeFileLocker.lockDir(tmpScriptCache);
+ } catch (FileNotFoundException e) {
+ throw new BuildException("A folder just created has been deleted: "
+ + tmpScriptCache.getAbsolutePath());
+ } catch (IOException e) {
+ throw new BuildException(
+ "A lock could not be obtained on the folder "
+ + tmpScriptCache.getAbsolutePath());
+ }
+ }
+
+ /**
+ * Build the cached version of the build file
+ *
+ * @param r
+ * @param binding
+ * @param parent
+ * @param cacheDir
+ * @return
+ */
+ private Script buildCache(Resource r, Binding binding, ClassLoader parent,
+ File cacheDir) {
+ writeTimestamp(r, cacheDir);
+ return parseScript(r, binding, parent, cacheDir);
+ }
+
+ /**
+ * Load the build file from the already compile classes in the cache
+ *
+ * @param r
+ * @param scriptCache
+ * @param binding
+ * @param parent
+ * @return
+ */
+ private Script loadFromCache(Resource r, File scriptCache, Binding binding,
+ ClassLoader parent) {
+ URLClassLoader scriptLoader;
+ try {
+ scriptLoader = new URLClassLoader(new URL[] { scriptCache.toURI()
+ .toURL() }, parent);
+ } catch (MalformedURLException e) {
+ // should not happen
+ throw new RuntimeException(e);
+ }
+ Class scriptClass;
+ try {
+ scriptClass = scriptLoader.loadClass(asJavaClass(r.getName()));
+ } catch (ClassNotFoundException e) {
+ throw new BuildException(
+ "The cache is corrupted, the script could not be loaded",
e);
+ }
+ return InvokerHelper.createScript(scriptClass, binding);
+ }
+
+ /**
+ * Write the timestamp of the resource in the cache.
+ *
+ * @param r
+ * @param scriptCache
+ */
+ private void writeTimestamp(Resource r, File scriptCache) {
+ File scriptInfoFile = new File(scriptCache, TIMESTAMP_FILE);
+ long timestamp = r.getLastModified();
+ PrintWriter writer;
+ try {
+ writer = new PrintWriter(new OutputStreamWriter(
+ new FileOutputStream(scriptInfoFile)), false);
+ } catch (FileNotFoundException e) {
+ throw new BuildException(
+ "The groovyfront cache for the build file '"
+ + r
+ + "' could not be created, impossible to create
the file '"
+ + scriptInfoFile.getAbsolutePath() + "'");
+ }
+ try {
+ writer.println(timestamp);
+ } finally {
+ writer.close();
+ }
+ if (writer.checkError()) {
+ throw new BuildException(
+ "The groovyfront cache for the build file '"
+ + r
+ + "' could not be created, error encountered while
writing the file '"
+ + scriptInfoFile.getAbsolutePath() + "'");
+ }
+ }
+
+ /**
+ * Read the timestamp stored in the cache. On any error,
+ * Resource.UNKNOWN_DATETIME is returned.
+ *
+ * @param r
+ * @param scriptCache
+ * @return
+ */
+ private long readTimestamp(Resource r, File scriptCache) {
+ File scriptInfoFile = new File(scriptCache, TIMESTAMP_FILE);
+ BufferedReader reader;
+ try {
+ reader = new BufferedReader(new FileReader(scriptInfoFile));
+ } catch (FileNotFoundException e) {
+ return Resource.UNKNOWN_DATETIME;
+ }
+ try {
+ return Long.parseLong(reader.readLine());
+ } catch (NumberFormatException e) {
+ return Resource.UNKNOWN_DATETIME;
+ } catch (IOException e) {
+ return Resource.UNKNOWN_DATETIME;
+ } finally {
+ try {
+ reader.close();
+ } catch (IOException e) {
+ // don't care
+ }
+ // touch the file to make the cache cleaner consider this file as
+ // recently used
+ scriptInfoFile.setLastModified(System.currentTimeMillis());
+ }
+ }
+
+ /**
+ * Turn the build file name into a compatible Java class name, the same way
+ * as it is mapped into {...@link #asGroovyClass(String)}
+ *
+ * @param filename
+ * @return
+ */
+ private String asJavaClass(String filename) {
+ String groovyClass = asGroovyClass(filename);
+ if (groovyClass.toLowerCase().endsWith(".groovy")) {
+ return groovyClass.substring(0, groovyClass.length() - 7);
+ }
+ return groovyClass;
+ }
+
+ /**
+ * Encode a byte array into hexadecimal encoding, a safe encoding for case
+ * insensitive file systems
+ *
+ * @param data
+ * @return
+ */
+ private String encodeHex(byte[] data) {
+ // code from apache commons-codec
+ int l = data.length;
+ char[] out = new char[l << 1];
+ for (int i = 0, j = 0; i < l; i++) {
+ out[j++] = HEX_DIGITS[(0xF0 & data[i]) >>> 4];
+ out[j++] = HEX_DIGITS[0x0F & data[i]];
+ }
+ return new String(out);
+ }
+
+}
Propchange:
ant/sandbox/groovyfront/src/main/java/org/apache/ant/groovyfront/cache/CachedGroovyScriptLoader.java
------------------------------------------------------------------------------
svn:eol-style = native
Propchange:
ant/sandbox/groovyfront/src/main/java/org/apache/ant/groovyfront/cache/CachedGroovyScriptLoader.java
------------------------------------------------------------------------------
svn:keywords = Date Revision Author HeadURL Id
Propchange:
ant/sandbox/groovyfront/src/main/java/org/apache/ant/groovyfront/cache/CachedGroovyScriptLoader.java
------------------------------------------------------------------------------
svn:mime-type = text/plain
Added:
ant/sandbox/groovyfront/src/main/java/org/apache/ant/groovyfront/cache/GroovyScriptCacheCleaner.java
URL:
http://svn.apache.org/viewvc/ant/sandbox/groovyfront/src/main/java/org/apache/ant/groovyfront/cache/GroovyScriptCacheCleaner.java?rev=980696&view=auto
==============================================================================
---
ant/sandbox/groovyfront/src/main/java/org/apache/ant/groovyfront/cache/GroovyScriptCacheCleaner.java
(added)
+++
ant/sandbox/groovyfront/src/main/java/org/apache/ant/groovyfront/cache/GroovyScriptCacheCleaner.java
Fri Jul 30 09:14:11 2010
@@ -0,0 +1,152 @@
+package org.apache.ant.groovyfront.cache;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.io.IOException;
+import java.nio.channels.FileLock;
+import java.util.Arrays;
+import java.util.Comparator;
+
+import org.apache.tools.ant.Project;
+
+/**
+ * Clean old entries in the cache, and don't keep more than some amount of
them.
+ */
+public class GroovyScriptCacheCleaner implements Runnable {
+
+ private static volatile Thread cleanerThread;
+
+ private static volatile long lastLaunched = 0;
+
+ private int maxToKeep = 1000;
+
+ // default is one month
+ private long timetolive = 1000 * 60 * 60 * 24 * 31;
+
+ private final Project project;
+
+ private final File cacheDir;
+
+ public GroovyScriptCacheCleaner(Project project, File cacheDir) {
+ this.project = project;
+ this.cacheDir = cacheDir;
+ }
+
+ public void setMaxToKeep(int maxToKeep) {
+ this.maxToKeep = maxToKeep;
+ }
+
+ public void setTimetolive(long timetolive) {
+ this.timetolive = timetolive;
+ }
+
+ public void run() {
+ try {
+ clean();
+ } catch (Throwable t) {
+ project.log("Some errors occurs while cleaning the cache", t,
+ Project.MSG_WARN);
+ } finally {
+ cleanerThread = null;
+ }
+ }
+
+ private void clean() {
+ int max = maxToKeep;
+ final long time = System.currentTimeMillis() - timetolive;
+
+ if (!cacheDir.exists() || !cacheDir.isDirectory()) {
+ // not a proper cache directory: abort
+ return;
+ }
+
+ // get cache entries which are older enough
+ File[] cacheEntries = cacheDir.listFiles(new FileFilter() {
+ public boolean accept(File f) {
+ if (!f.isDirectory()) {
+ return false;
+ }
+ long modified = getLastReadTimestamp(f);
+ if (modified == 0 || modified > time) {
+ return false;
+ }
+ return true;
+ }
+ });
+
+ if (cacheEntries.length < max) {
+ // not reaching max
+ return;
+ }
+
+ // order by time of last read
+ Arrays.sort(cacheEntries, new Comparator() {
+ public int compare(Object dir1, Object dir2) {
+ long modified1 = getLastReadTimestamp((File) dir1);
+ long modified2 = getLastReadTimestamp((File) dir1);
+ long diff = modified2 - modified1;
+ return diff > 0 ? 1 : diff < 0 ? -1 : 0;
+ }
+ });
+
+ for (int i = 0; i < cacheEntries.length - max; i++) {
+ delete(cacheEntries[i]);
+ }
+ }
+
+ private void delete(File dir) {
+ FileLock lock;
+ try {
+ lock = ThreadSafeFileLocker.tryLock(dir);
+ } catch (IOException e) {
+ // no any issue, just skip that delete
+ return;
+ }
+ if (lock == null) {
+ // cache entry being read, skip the delete
+ return;
+ }
+ try {
+ CachedGroovyScriptLoader.cleanCacheDir(dir);
+ } catch (IOException e) {
+ project.log(
+ "Some groovy script cache entries might not have been
deleted correctly in "
+ + dir.getAbsolutePath(), e, Project.MSG_WARN);
+ } finally {
+ ThreadSafeFileLocker.release(dir, lock);
+ }
+ }
+
+ /**
+ * Get the timestamp of the last read of that folder. It correspond to the
+ * timestamp of the file containing the original build file timestamp.
+ *
+ * @param dir
+ * @return
+ */
+ private long getLastReadTimestamp(File dir) {
+ File timestampFile = new File(dir,
+ CachedGroovyScriptLoader.TIMESTAMP_FILE);
+ return timestampFile.lastModified();
+ }
+
+ public static synchronized void launchClean(Project project, File cacheDir,
+ long minCleanerPeriod, Long timetolive, Integer maxToKeep) {
+ if (cleanerThread == null
+ && lastLaunched < System.currentTimeMillis() -
minCleanerPeriod) {
+ lastLaunched = System.currentTimeMillis();
+ GroovyScriptCacheCleaner cleaner = new GroovyScriptCacheCleaner(
+ project, cacheDir);
+ if (timetolive != null) {
+ cleaner.setTimetolive(timetolive.longValue());
+ }
+ if (maxToKeep != null) {
+ cleaner.setMaxToKeep(maxToKeep.intValue());
+ }
+ cleanerThread = new Thread(cleaner);
+ cleanerThread.setPriority(Thread.MIN_PRIORITY);
+ cleanerThread.setDaemon(true);
+ cleanerThread.run();
+ }
+ }
+}
Propchange:
ant/sandbox/groovyfront/src/main/java/org/apache/ant/groovyfront/cache/GroovyScriptCacheCleaner.java
------------------------------------------------------------------------------
svn:eol-style = native
Propchange:
ant/sandbox/groovyfront/src/main/java/org/apache/ant/groovyfront/cache/GroovyScriptCacheCleaner.java
------------------------------------------------------------------------------
svn:keywords = Date Revision Author HeadURL Id
Propchange:
ant/sandbox/groovyfront/src/main/java/org/apache/ant/groovyfront/cache/GroovyScriptCacheCleaner.java
------------------------------------------------------------------------------
svn:mime-type = text/plain
Added:
ant/sandbox/groovyfront/src/main/java/org/apache/ant/groovyfront/cache/GroovyScriptLoader.java
URL:
http://svn.apache.org/viewvc/ant/sandbox/groovyfront/src/main/java/org/apache/ant/groovyfront/cache/GroovyScriptLoader.java?rev=980696&view=auto
==============================================================================
---
ant/sandbox/groovyfront/src/main/java/org/apache/ant/groovyfront/cache/GroovyScriptLoader.java
(added)
+++
ant/sandbox/groovyfront/src/main/java/org/apache/ant/groovyfront/cache/GroovyScriptLoader.java
Fri Jul 30 09:14:11 2010
@@ -0,0 +1,72 @@
+package org.apache.ant.groovyfront.cache;
+
+import groovy.lang.Binding;
+import groovy.lang.GroovyShell;
+import groovy.lang.Script;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+import org.apache.tools.ant.BuildException;
+import org.apache.tools.ant.types.Resource;
+import org.codehaus.groovy.control.CompilationFailedException;
+import org.codehaus.groovy.control.CompilerConfiguration;
+
+public class GroovyScriptLoader {
+
+ public Script loadScript(Resource r, Binding binding, ClassLoader parent) {
+ return parseScript(r, binding, parent, null);
+ }
+
+ /**
+ * Parse and compile the build file. If the provided scriptCache is not
+ * <code>null</code>, then the resulting classes will be put in that folder
+ * for later reloading.
+ *
+ * @param r
+ * @param binding
+ * @param classLoader
+ * @param scriptCache
+ * @return
+ */
+ protected Script parseScript(Resource r, Binding binding,
+ ClassLoader classLoader, File scriptCache) {
+ CompilerConfiguration config = new CompilerConfiguration(
+ CompilerConfiguration.DEFAULT);
+ config.setTargetDirectory(scriptCache);
+ GroovyShell groovyShell = new GroovyShell(classLoader, binding,
config);
+ InputStream in;
+ try {
+ in = r.getInputStream();
+ } catch (IOException e) {
+ throw new BuildException("Error reading groovy file " + r + ": "
+ + e.getMessage(), e);
+ }
+ try {
+ return groovyShell.parse(new InputStreamReader(in),
+ asGroovyClass(r.getName()));
+ } catch (CompilationFailedException e) {
+ throw new BuildException("Error reading groovy file " + r + ": "
+ + e.getMessage(), e);
+ } finally {
+ try {
+ in.close();
+ } catch (IOException e) {
+ // don't care
+ }
+ }
+ }
+
+ /**
+ * Turn the build file name into a compatible Groovy file name
+ *
+ * @param filename
+ * @return
+ */
+ protected String asGroovyClass(String filename) {
+ return filename.replaceAll("-", "_");
+ }
+
+}
Propchange:
ant/sandbox/groovyfront/src/main/java/org/apache/ant/groovyfront/cache/GroovyScriptLoader.java
------------------------------------------------------------------------------
svn:eol-style = native
Propchange:
ant/sandbox/groovyfront/src/main/java/org/apache/ant/groovyfront/cache/GroovyScriptLoader.java
------------------------------------------------------------------------------
svn:keywords = Date Revision Author HeadURL Id
Propchange:
ant/sandbox/groovyfront/src/main/java/org/apache/ant/groovyfront/cache/GroovyScriptLoader.java
------------------------------------------------------------------------------
svn:mime-type = text/plain
Added:
ant/sandbox/groovyfront/src/main/java/org/apache/ant/groovyfront/cache/ThreadSafeFileLocker.java
URL:
http://svn.apache.org/viewvc/ant/sandbox/groovyfront/src/main/java/org/apache/ant/groovyfront/cache/ThreadSafeFileLocker.java?rev=980696&view=auto
==============================================================================
---
ant/sandbox/groovyfront/src/main/java/org/apache/ant/groovyfront/cache/ThreadSafeFileLocker.java
(added)
+++
ant/sandbox/groovyfront/src/main/java/org/apache/ant/groovyfront/cache/ThreadSafeFileLocker.java
Fri Jul 30 09:14:11 2010
@@ -0,0 +1,121 @@
+package org.apache.ant.groovyfront.cache;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.channels.FileLock;
+import java.nio.channels.OverlappingFileLockException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * {...@link FileLock} are great to deal with synchronization between jvms, but
+ * within a single jvm some {...@link OverlappingFileLockException} is raised
if a
+ * file lock is tried to be acquired several times on the same file. So the
file
+ * lock is "surrounded" with a sort of intra jvm memory lock.
+ */
+public class ThreadSafeFileLocker {
+
+ private static final String LOCK_FILENAME = ".lock";
+
+ // static because the locking should be done on the entire jvm
+ private static final Map lockedFiles = new HashMap();
+
+ public static FileLock tryLock(File f) throws FileNotFoundException,
+ IOException {
+ String absolutePath = f.getAbsolutePath();
+ RandomAccessFile lockedFile;
+ synchronized (lockedFiles) {
+ lockedFile = (RandomAccessFile) lockedFiles.get(absolutePath);
+ if (lockedFile != null) {
+ // the file is already locked by this jvm, or being tried to be
+ // locked. Either way, we won't have the file lock
+ return null;
+ }
+ lockedFile = new RandomAccessFile(f, "rw");
+ lockedFiles.put(absolutePath, lockedFile);
+ }
+ FileLock lock = null;
+ try {
+ lock = lockedFile.getChannel().tryLock();
+ } finally {
+ if (lock == null) {
+ // we didn't get the lock
+ synchronized (lockedFiles) {
+ lockedFiles.remove(absolutePath);
+ }
+ }
+ }
+ return lock;
+ }
+
+ public static FileLock lock(File f) throws FileNotFoundException,
+ IOException {
+ String absolutePath = f.getAbsolutePath();
+ RandomAccessFile lockedFile;
+ synchronized (lockedFiles) {
+ lockedFile = (RandomAccessFile) lockedFiles.get(absolutePath);
+ if (lockedFile != null) {
+ // FIXME here we should somehow wait and retry
+ return null;
+ }
+ lockedFile = new RandomAccessFile(f, "rw");
+ lockedFiles.put(absolutePath, lockedFile);
+ }
+ FileLock lock = null;
+ try {
+ lock = lockedFile.getChannel().lock();
+ } finally {
+ if (lock == null) {
+ // we didn't get the lock
+ synchronized (lockedFiles) {
+ lockedFiles.remove(absolutePath);
+ }
+ }
+ }
+ return lock;
+ }
+
+ public static void release(File f, FileLock lock) {
+ String absolutePath = f.getAbsolutePath();
+ RandomAccessFile lockedFile = (RandomAccessFile) lockedFiles
+ .get(absolutePath);
+ if (lockedFile == null) {
+ throw new IllegalStateException(
+ "The file "
+ + f
+ + " should have been sucessfully locked before
unlocking it");
+ }
+ try {
+ lock.release();
+ } catch (IOException e) {
+ // don't care
+ } finally {
+ // release the file and the jvm "lock"
+ try {
+ lockedFile.close();
+ } catch (IOException e) {
+ // don't care
+ } finally {
+ synchronized (lockedFiles) {
+ lockedFiles.remove(absolutePath);
+ }
+ }
+ }
+ }
+
+ public static FileLock tryLockDir(File dir) throws FileNotFoundException,
+ IOException {
+ return tryLock(new File(dir, LOCK_FILENAME));
+ }
+
+ public static FileLock lockDir(File dir) throws FileNotFoundException,
+ IOException {
+ return lock(new File(dir, LOCK_FILENAME));
+ }
+
+ public static void releaseDir(File dir, FileLock lock) {
+ release(new File(dir, LOCK_FILENAME), lock);
+ }
+}
Propchange:
ant/sandbox/groovyfront/src/main/java/org/apache/ant/groovyfront/cache/ThreadSafeFileLocker.java
------------------------------------------------------------------------------
svn:eol-style = native
Propchange:
ant/sandbox/groovyfront/src/main/java/org/apache/ant/groovyfront/cache/ThreadSafeFileLocker.java
------------------------------------------------------------------------------
svn:keywords = Date Revision Author HeadURL Id
Propchange:
ant/sandbox/groovyfront/src/main/java/org/apache/ant/groovyfront/cache/ThreadSafeFileLocker.java
------------------------------------------------------------------------------
svn:mime-type = text/plain
Added:
ant/sandbox/groovyfront/src/test/java/org/apache/ant/groovyfront/cache/GroovyScriptCacheTest.java
URL:
http://svn.apache.org/viewvc/ant/sandbox/groovyfront/src/test/java/org/apache/ant/groovyfront/cache/GroovyScriptCacheTest.java?rev=980696&view=auto
==============================================================================
---
ant/sandbox/groovyfront/src/test/java/org/apache/ant/groovyfront/cache/GroovyScriptCacheTest.java
(added)
+++
ant/sandbox/groovyfront/src/test/java/org/apache/ant/groovyfront/cache/GroovyScriptCacheTest.java
Fri Jul 30 09:14:11 2010
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.ant.groovyfront.cache;
+
+import groovy.lang.Binding;
+import groovy.lang.GroovyShell;
+import groovy.lang.Script;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.Reader;
+
+import junit.framework.TestCase;
+
+import org.apache.ant.groovyfront.cache.CachedGroovyScriptLoader;
+import org.apache.tools.ant.types.resources.FileResource;
+import org.apache.tools.ant.util.FileUtils;
+
+public class GroovyScriptCacheTest extends TestCase {
+
+ private File groovyFile;
+ private File helloFile;
+ private File tmpDir;
+
+ protected void setUp() throws Exception {
+ tmpDir = File.createTempFile("GroovyScriptCacheTest", "");
+ tmpDir.delete();
+ tmpDir.mkdir();
+ System.out.println("tmp dir: " + tmpDir.getAbsolutePath());
+
+ groovyFile = new File(tmpDir, "test.groovy");
+ helloFile = new File(tmpDir, "hello.txt");
+
+ PrintWriter writer = new PrintWriter(groovyFile);
+ writer.println("new File(\"" + helloFile.getAbsolutePath()
+ + "\").write(\"Hello world\")");
+ writer.close();
+ }
+
+ protected void tearDown() throws Exception {
+ clean(tmpDir);
+ }
+
+ private void clean(File dir) throws IOException {
+ File[] files = dir.listFiles();
+ for (int i = 0; i < files.length; i++) {
+ if (files[i].isDirectory()) {
+ clean(files[i]);
+ files[i].delete();
+ } else {
+ FileUtils.getFileUtils().tryHardToDelete(files[i]);
+ }
+ }
+ }
+
+ public void testCache() throws Exception {
+ assertFalse(helloFile.exists());
+
+ long t = System.currentTimeMillis();
+ GroovyShell shell = new GroovyShell();
+ System.out.println("groovy init: " + (System.currentTimeMillis() - t)
+ + " ms");
+ t = System.currentTimeMillis();
+ shell.evaluate(groovyFile);
+ System.out.println("hadoc run: " + (System.currentTimeMillis() - t)
+ + " ms");
+
+ assertTrue(helloFile.exists());
+ Reader reader = new FileReader(helloFile);
+ String content = FileUtils.readFully(reader);
+ reader.close();
+ assertEquals("Hello world", content);
+
+ helloFile.delete();
+ assertFalse(helloFile.exists());
+
+ CachedGroovyScriptLoader cache = new CachedGroovyScriptLoader(tmpDir);
+ t = System.currentTimeMillis();
+ Script script = cache.loadScript(new FileResource(groovyFile),
+ new Binding(), this.getClass().getClassLoader());
+ script.run();
+ System.out.println("cache miss run: "
+ + (System.currentTimeMillis() - t) + " ms");
+
+ assertTrue(helloFile.exists());
+ reader = new FileReader(helloFile);
+ content = FileUtils.readFully(reader);
+ reader.close();
+ assertEquals("Hello world", content);
+
+ helloFile.delete();
+ assertFalse(helloFile.exists());
+
+ t = System.currentTimeMillis();
+ script = cache.loadScript(new FileResource(groovyFile), new Binding(),
+ this.getClass().getClassLoader());
+ script.run();
+ System.out.println("cache hit run: " + (System.currentTimeMillis() - t)
+ + " ms");
+
+ assertTrue(helloFile.exists());
+ reader = new FileReader(helloFile);
+ content = FileUtils.readFully(reader);
+ reader.close();
+ assertEquals("Hello world", content);
+ }
+}
Propchange:
ant/sandbox/groovyfront/src/test/java/org/apache/ant/groovyfront/cache/GroovyScriptCacheTest.java
------------------------------------------------------------------------------
svn:eol-style = native
Propchange:
ant/sandbox/groovyfront/src/test/java/org/apache/ant/groovyfront/cache/GroovyScriptCacheTest.java
------------------------------------------------------------------------------
svn:keywords = Date Revision Author HeadURL Id
Propchange:
ant/sandbox/groovyfront/src/test/java/org/apache/ant/groovyfront/cache/GroovyScriptCacheTest.java
------------------------------------------------------------------------------
svn:mime-type = text/plain