Hi there,
In the Java unit test listed further below, I create 100 GroovyShell
instances and add the same directory to the classpath of the
GroovyClassLoader of each GroovyShell. This directory contains 100
script files, named Util0.groovy to Util99.groovy, containing the
following script (with XX replaced by 0..99):
@Grab('com.google.guava:guava:18.0')
import com.google.common.base.Ascii
class UtilXX {
static boolean isUpperCase(def c) {
return Ascii.isUpperCase(c)\n"
}
}
The unit test then runs
shell.evaluate("return Util" + j + ".isUpperCase('C' as char)");
in 100 separate threads (with j from 0..99).
This test does not always fail, but often. Most of the time, a
ConcurrentModificationException occurs down in ivy (which is used by
Grape). In other cases, a MultipleCompilationErrorsException ocurred at
"@Grab('com.google.guava:guava:18.0')". (Full stacktraces at the bottom.)
It looks to me like Grape resp. ivy is not thread safe.
Question 1: Is that a bug or just how things currently are? (If it's a
bug, rather Grape bug or an ivy bug, resp. where would I best file it?)
Question 2: If it's not a bug, is that a general issue, i.e. if I
compile two sets of sources with the GroovyCompiler (say I create a
CompilationUnit instance and add sources) will there be similar issues
if both sets of sources have the same Grape dependencies? (Yes, I know,
I could just try, but maybe the answer obvious to someone who knows the
implementation.)
(I have tested this with Groovy 2.4.3 and ivy 2.4.0 on MacOS X with Java
6, but also I have also seen the same issues on CentOS with several
Groovy/ivy versions and Java 7, on a deployed webapp, so it seems to be
fairly independent of the exact environment.)
Best wishes,
Alain
-------------------------------------
Java Unit Test (does not always fail)
-------------------------------------
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import groovy.lang.GroovyShell;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.util.LinkedList;
import java.util.List;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
public class GrapeAndGroovyShellConcurrencyTest {
@Rule
public TemporaryFolder tempFolder = new TemporaryFolder();
private volatile boolean testPassed;
@Before
public void setUp() {
testPassed = true;
}
public static void setFileText(File file, String text)
throws FileNotFoundException, UnsupportedEncodingException {
PrintWriter writer = new PrintWriter(file, "UTF-8");
writer.write(text);
writer.close();
}
@Test
public void testConcurrency() throws Exception {
File dir = tempFolder.getRoot();
final int n = 100;
for (int i=0; i<n; i++) {
File f1 = new File(dir, "Util" + i + ".groovy");
setFileText(f1, "@Grab('com.google.guava:guava:18.0')\n"
+ "import com.google.common.base.Ascii\n"
+ "class Util" + i + " {\n"
+ " static boolean isUpperCase(def c) {\n"
+ " return Ascii.isUpperCase(c)\n"
+ " }\n"
+ "}\n"
);
}
List<Thread> scriptThreads = new LinkedList<Thread>();
for (int i=0; i<n; i++) {
final GroovyShell shell = new GroovyShell();
shell.getClassLoader().addClasspath(dir.getAbsolutePath());
final int j = i;
Thread scriptThread = new Thread(
new Runnable() {
public void run() {
try {
assertEquals(true,
shell.evaluate("return Util" + j + ".isUpperCase('C' as char)"));
System.out.println("Thread " +
Thread.currentThread().getName() + " : OK");
} catch (Throwable t) {
System.out.println("Thread " +
Thread.currentThread().getName() + " : FAILED");
t.printStackTrace();
testPassed = false;
}
}
});
scriptThread.setDaemon(true);
scriptThread.setName("run-" + i);
scriptThread.start();
scriptThreads.add(scriptThread);
}
for (Thread scriptThread : scriptThreads) {
scriptThread.join();
}
assertTrue("must be true", testPassed);
}
}
------------
Stacktrace 1
------------
org.codehaus.groovy.control.MultipleCompilationErrorsException: startup
failed:
General error during conversion: java.util.ConcurrentModificationException
java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:859)
at java.util.ArrayList$Itr.next(ArrayList.java:831)
at
org.apache.ivy.util.MessageLoggerHelper.sumupProblems(MessageLoggerHelper.java:45)
at
org.apache.ivy.util.MessageLoggerEngine.sumupProblems(MessageLoggerEngine.java:136)
at org.apache.ivy.util.Message.sumupProblems(Message.java:143)
at
org.apache.ivy.core.resolve.ResolveEngine.resolve(ResolveEngine.java:347)
at org.apache.ivy.Ivy.resolve(Ivy.java:523)
at org.apache.ivy.Ivy$resolve$1.call(Unknown Source)
at groovy.grape.GrapeIvy.getDependencies(GrapeIvy.groovy:404)
at sun.reflect.GeneratedMethodAccessor671.invoke(Unknown Source)
at
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:606)
at
org.codehaus.groovy.runtime.callsite.PogoMetaMethodSite$PogoCachedMethodSite.invoke(PogoMetaMethodSite.java:166)
at
org.codehaus.groovy.runtime.callsite.PogoMetaMethodSite.callCurrent(PogoMetaMethodSite.java:56)
at groovy.grape.GrapeIvy.resolve(GrapeIvy.groovy:563)
at groovy.grape.GrapeIvy$resolve$56.callCurrent(Unknown Source)
at groovy.grape.GrapeIvy.resolve(GrapeIvy.groovy:532)
at groovy.grape.GrapeIvy$resolve$45.callCurrent(Unknown Source)
at
org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCallCurrent(CallSiteArray.java:49)
at
org.codehaus.groovy.runtime.callsite.AbstractCallSite.callCurrent(AbstractCallSite.java:151)
at
org.codehaus.groovy.runtime.callsite.AbstractCallSite.callCurrent(AbstractCallSite.java:179)
at groovy.grape.GrapeIvy.grab(GrapeIvy.groovy:254)
at groovy.grape.Grape.grab(Grape.java:163)
at
groovy.grape.GrabAnnotationTransformation.visit(GrabAnnotationTransformation.java:358)
at
org.codehaus.groovy.transform.ASTTransformationVisitor$3.call(ASTTransformationVisitor.java:319)
at
org.codehaus.groovy.control.CompilationUnit.applyToSourceUnits(CompilationUnit.java:928)
at
org.codehaus.groovy.control.CompilationUnit.doPhaseOperation(CompilationUnit.java:590)
at
org.codehaus.groovy.control.CompilationUnit.processPhaseOperations(CompilationUnit.java:566)
at
org.codehaus.groovy.control.CompilationUnit.compile(CompilationUnit.java:543)
at
groovy.lang.GroovyClassLoader.doParseClass(GroovyClassLoader.java:297)
at groovy.lang.GroovyClassLoader.parseClass(GroovyClassLoader.java:267)
at groovy.lang.GroovyShell.parseClass(GroovyShell.java:692)
at groovy.lang.GroovyShell.parse(GroovyShell.java:704)
at groovy.lang.GroovyShell.evaluate(GroovyShell.java:588)
at groovy.lang.GroovyShell.evaluate(GroovyShell.java:627)
at groovy.lang.GroovyShell.evaluate(GroovyShell.java:598)
at
GrapeAndGroovyShellConcurrencyTest$1.run(GrapeAndGroovyShellConcurrencyTest.java:64)
at java.lang.Thread.run(Thread.java:745)
1 error
at
org.codehaus.groovy.control.ErrorCollector.failIfErrors(ErrorCollector.java:309)
at
org.codehaus.groovy.control.ErrorCollector.addException(ErrorCollector.java:155)
at
org.codehaus.groovy.control.SourceUnit.addException(SourceUnit.java:345)
at
groovy.grape.GrabAnnotationTransformation.visit(GrabAnnotationTransformation.java:367)
at
org.codehaus.groovy.transform.ASTTransformationVisitor$3.call(ASTTransformationVisitor.java:319)
at
org.codehaus.groovy.control.CompilationUnit.applyToSourceUnits(CompilationUnit.java:928)
at
org.codehaus.groovy.control.CompilationUnit.doPhaseOperation(CompilationUnit.java:590)
at
org.codehaus.groovy.control.CompilationUnit.processPhaseOperations(CompilationUnit.java:566)
at
org.codehaus.groovy.control.CompilationUnit.compile(CompilationUnit.java:543)
at
groovy.lang.GroovyClassLoader.doParseClass(GroovyClassLoader.java:297)
at groovy.lang.GroovyClassLoader.parseClass(GroovyClassLoader.java:267)
at groovy.lang.GroovyShell.parseClass(GroovyShell.java:692)
at groovy.lang.GroovyShell.parse(GroovyShell.java:704)
at groovy.lang.GroovyShell.evaluate(GroovyShell.java:588)
at groovy.lang.GroovyShell.evaluate(GroovyShell.java:627)
at groovy.lang.GroovyShell.evaluate(GroovyShell.java:598)
at
GrapeAndGroovyShellConcurrencyTest$1.run(GrapeAndGroovyShellConcurrencyTest.java:64)
at java.lang.Thread.run(Thread.java:745)
------------
Stacktrace 2
------------
org.codehaus.groovy.control.MultipleCompilationErrorsException: startup
failed:
file:/var/folders/38/r0n49vmn7zg5dffk79_tgpl80000gn/T/junit124846036508580912/Util7.groovy:
1: unable to resolve class com.google.common.base.Ascii
@ line 1, column 1.
@Grab('com.google.guava:guava:18.0')
^
1 error
at
org.codehaus.groovy.control.ErrorCollector.failIfErrors(ErrorCollector.java:309)
at
org.codehaus.groovy.control.CompilationUnit.applyToSourceUnits(CompilationUnit.java:943)
at
org.codehaus.groovy.control.CompilationUnit.doPhaseOperation(CompilationUnit.java:590)
at
org.codehaus.groovy.control.CompilationUnit.compile(CompilationUnit.java:539)
at
groovy.lang.GroovyClassLoader.doParseClass(GroovyClassLoader.java:297)
at groovy.lang.GroovyClassLoader.parseClass(GroovyClassLoader.java:267)
at groovy.lang.GroovyShell.parseClass(GroovyShell.java:692)
at groovy.lang.GroovyShell.parse(GroovyShell.java:704)
at groovy.lang.GroovyShell.evaluate(GroovyShell.java:588)
at groovy.lang.GroovyShell.evaluate(GroovyShell.java:627)
at groovy.lang.GroovyShell.evaluate(GroovyShell.java:598)
at
GrapeAndGroovyShellConcurrencyTest$1.run(GrapeAndGroovyShellConcurrencyTest.java:64)
at java.lang.Thread.run(Thread.java:745)