Hi again,
Looks like everybody is busy with stuff on a somewhat lower level of
Maslov's Pyramid? ;)
So, I will simply answer myself to my questions:
> 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?)
I consider that a bug, even though it seems to me that fixing it might
possibly be not so simple...
I created an account for the Groovy Apache JIRA, resp. seems like my
user had been already transferred there (or maybe I created an account
in the past and forgotten about it), with the intent to file a bug with
Groovy, but apparently I don't have the rights to do so?
If its possible without jumping through too many hoops, I would still be
available to file the bug (how?), else this post will have to suffice... ;)
> 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.)
Yes, it is definitely a general issue, see unit test further below which
uses CompilationUnit directly.
Question 3 (new): Am I stuck?
No, I am not, at least not with Grengine: http://grengine.ch
I can simply override DefaultGroovyCompiler with a class that
synchronizes around CompilationUnit.compile(), since Grengine is not
using GroovyClassLoader, GroovyShell or GroovyScriptEngine, except a
GroovyClassLoader during compilation. (Of course, in order to use Grape,
a GroovyClassLoader has to be in the classpath at runtime (or a
RootLoader, actually is there any real reason for that?), but that is
also possible to do by configuring Grengine.)
Or is there a workaround that works with Groovy itself, on the level of
the compiler or GroovyClassLoader/GroovyShell/GroovyScriptEngine, except
synchronizing each an every call that might compile anything?
If not, I will probably enhance Grengine to make the workaround simpler,
independently of whether this will be fixed in the future, in order to
provide a workaround for older Groovy versions.
Best wishes,
Alain
Here is the Java unit test:
import static org.junit.Assert.assertTrue;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import org.codehaus.groovy.control.CompilationUnit;
import org.codehaus.groovy.control.Phases;
import org.junit.Test;
public class GroovyCompileConcurrencyTest {
@Test
public void testConcurrencyLowLevel() throws Exception {
final List<Throwable> errors = Collections.synchronizedList(new
LinkedList<Throwable>());
final int nLoops = 1000;
final int nThreads = 10;
for (int i=0; i<nLoops; i++) {
System.out.print(".");
List<Thread> scriptThreads = new LinkedList<Thread>();
for (int j=0; j<nThreads; j++) {
final CompilationUnit cu = new CompilationUnit();
cu.addSource("Whatever",
"@Grab('com.google.guava:guava:18.0')\n" +
"import com.google.common.base.Ascii\n");
Thread scriptThread = new Thread(
new Runnable() {
public void run() {
try {
cu.compile(Phases.CLASS_GENERATION);
} catch (Throwable t) {
errors.add(t);
t.printStackTrace();
}
}
});
scriptThreads.add(scriptThread);
}
for (Thread scriptThread : scriptThreads) {
scriptThread.start();
}
for (Thread scriptThread : scriptThreads) {
scriptThread.join();
}
assertTrue("must be true", errors.size() == 0);
}
}
}
On 26.04.15 17:52, Alain Stalder wrote:
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)
.