ant-notifications mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From hi...@apache.org
Subject svn commit: r980696 - in /ant/sandbox/groovyfront/src: main/java/org/apache/ant/groovyfront/ main/java/org/apache/ant/groovyfront/cache/ test/java/org/apache/ant/groovyfront/cache/
Date Fri, 30 Jul 2010 09:14:12 GMT
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



Mime
View raw message