Make partial loading threadsafe across platforms

This commit is contained in:
Adam Gent
2023-11-28 10:35:02 -05:00
parent 09f712167c
commit ab4bcb749f
2 changed files with 86 additions and 4 deletions
@@ -13,6 +13,8 @@ import java.util.Arrays;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/** /**
* Provides <a href="http://mustache.github.com/">Mustache</a> templating services. * Provides <a href="http://mustache.github.com/">Mustache</a> templating services.
@@ -1057,10 +1059,20 @@ public class Mustache {
protected Template getTemplate () { protected Template getTemplate () {
// we compile our template lazily to avoid infinie recursion if a template includes // we compile our template lazily to avoid infinie recursion if a template includes
// itself (see issue #13) // itself (see issue #13)
if (_template == null) { Template t = _template;
_template = _comp.loadTemplate(_name).indent(_indent); if (t == null) {
// We cannot use synchronized or a CAS operation here since loadTemplate might be an IO call
// and virtual threads prefer regular locks.
lock.lock();
try {
if ((t = _template) == null) {
_template = t = _comp.loadTemplate(_name).indent(_indent);
} }
return _template; } finally {
lock.unlock();
}
}
return t;
} }
protected IncludedTemplateSegment indent(String indent, boolean first, boolean last) { protected IncludedTemplateSegment indent(String indent, boolean first, boolean last) {
// Indent this partial based on the spacing provided. // Indent this partial based on the spacing provided.
@@ -1084,7 +1096,8 @@ public class Mustache {
protected final Compiler _comp; protected final Compiler _comp;
protected final String _name; protected final String _name;
private final String _indent; private final String _indent;
private Template _template; private final Lock lock = new ReentrantLock();
private volatile Template _template;
protected boolean _standalone = false; protected boolean _standalone = false;
} }
@@ -0,0 +1,69 @@
package com.samskivert.mustache;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Test;
import com.samskivert.mustache.Mustache.TemplateLoader;
public class PartialThreadSafeTest {
@Test
public void testPartialThreadSafe() throws Exception {
long t = System.currentTimeMillis();
AtomicInteger loadCount = new AtomicInteger();
TemplateLoader loader = new TemplateLoader() {
@Override
public Reader getTemplate(String name) throws Exception {
if ("partial".equals(name)) {
loadCount.incrementAndGet();
TimeUnit.MILLISECONDS.sleep(20);
return new StringReader("Hello");
}
throw new IOException(name);
}
};
Template template = Mustache.compiler().withLoader(loader).compile("{{stuff}}\n\t{{> partial }}");
ExecutorService executor = Executors.newFixedThreadPool(64);
ConcurrentLinkedDeque<Exception> q = new ConcurrentLinkedDeque<>();
Map<String, Object> m = new HashMap<>();
m.put("stuff", "Foo");
for (int i = 100; i > 0; i--) {
int ii = i;
executor.execute(() -> {
try {
TimeUnit.MILLISECONDS.sleep(ii % 10);
template.execute(m);
} catch (Exception e) {
q.add(e);
}
});
}
executor.shutdown();
executor.awaitTermination(10_000, TimeUnit.MILLISECONDS);
if (!q.isEmpty()) {
System.out.println(q);
}
assertTrue(q.isEmpty());
assertEquals(1, loadCount.get());
System.out.println(loadCount);
System.out.println(System.currentTimeMillis() - t);
}
}