This is an automated email from the ASF dual-hosted git repository. henrib pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/commons-jexl.git
commit a4c4cc996b1405e1d93f8c59d0d4c3670aefffd2 Author: henrib <hen...@apache.org> AuthorDate: Mon Dec 2 17:08:09 2019 +0100 JEXL-317: added optional interface JexlContext.CancellationHandle Task #JEXL-317 - Support script cancellation through less invasive API --- RELEASE-NOTES.txt | 1 + .../java/org/apache/commons/jexl3/JexlContext.java | 15 +++++++ .../apache/commons/jexl3/internal/Interpreter.java | 3 +- .../commons/jexl3/internal/InterpreterBase.java | 18 ++++---- src/site/xdoc/changes.xml | 3 ++ .../apache/commons/jexl3/ScriptCallableTest.java | 51 +++++++++++++++++++++- 6 files changed, 81 insertions(+), 10 deletions(-) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 6a6ee8d..23ce03f 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -48,6 +48,7 @@ What's new in 3.2: New Features in 3.2: ==================== +* JEXL-317: Support script cancellation through less invasive API * JEXL-307: Variable redeclaration option * JEXL-295: Add unary plus operator * JEXL-292: Allow specifying custom Permissions class for Uberspect to be used later by Introspector diff --git a/src/main/java/org/apache/commons/jexl3/JexlContext.java b/src/main/java/org/apache/commons/jexl3/JexlContext.java index 2f4d69e..7a9c828 100644 --- a/src/main/java/org/apache/commons/jexl3/JexlContext.java +++ b/src/main/java/org/apache/commons/jexl3/JexlContext.java @@ -18,6 +18,7 @@ package org.apache.commons.jexl3; import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicBoolean; /** * Manages variables which can be referenced in a JEXL expression. @@ -175,4 +176,18 @@ public interface JexlContext { */ void processPragma(String key, Object value); } + + /** + * A marker interface of the JexlContext sharing a cancelling flag. + * <p>A script running in a thread can thus be notified through this reference + * of its cancellation through the context. It uses the same interpreter logic + * that reacts to cancellation and is an alternative to using callable() and/or + * interrupting script interpreter threads. + */ + interface CancellationHandle { + /** + * @return a cancelable boolean used by the interpreter + */ + AtomicBoolean getCancellation(); + } } diff --git a/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java b/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java index 9c27f61..dac5b49 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java +++ b/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java @@ -200,7 +200,8 @@ public class Interpreter extends InterpreterBase { } catch (JexlException.Return xreturn) { return xreturn.getValue(); } catch (JexlException.Cancel xcancel) { - cancelled |= Thread.interrupted(); + // cancelled |= Thread.interrupted(); + cancelled.weakCompareAndSet(false, Thread.interrupted()); if (isCancellable()) { throw xcancel.clean(); } diff --git a/src/main/java/org/apache/commons/jexl3/internal/InterpreterBase.java b/src/main/java/org/apache/commons/jexl3/internal/InterpreterBase.java index 504c1c1..3f4f2d0 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/InterpreterBase.java +++ b/src/main/java/org/apache/commons/jexl3/internal/InterpreterBase.java @@ -20,6 +20,7 @@ package org.apache.commons.jexl3.internal; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; import org.apache.commons.jexl3.JexlArithmetic; import org.apache.commons.jexl3.JexlContext; import org.apache.commons.jexl3.JexlEngine; @@ -63,7 +64,7 @@ public abstract class InterpreterBase extends ParserVisitor { /** Cache executors. */ protected final boolean cache; /** Cancellation support. */ - protected volatile boolean cancelled = false; + protected final AtomicBoolean cancelled; /** Empty parameters for method matching. */ protected static final Object[] EMPTY_PARAMS = new Object[0]; /** The context to store/retrieve variables. */ @@ -100,6 +101,11 @@ public abstract class InterpreterBase extends ParserVisitor { } else { ns = Engine.EMPTY_NS; } + AtomicBoolean acancel = null; + if (this.context instanceof JexlContext.CancellationHandle) { + acancel = ((JexlContext.CancellationHandle) context).getCancellation(); + } + this.cancelled = acancel != null? acancel : new AtomicBoolean(false); this.functions = jexl.functions; this.functors = null; this.operators = new Operators(this); @@ -118,6 +124,7 @@ public abstract class InterpreterBase extends ParserVisitor { arithmetic = ii.arithmetic; cache = ii.cache; ns = ii.ns; + cancelled = ii.cancelled; functions = ii.functions; functors = ii.functors; operators = ii.operators; @@ -544,12 +551,7 @@ public abstract class InterpreterBase extends ParserVisitor { * @return false if already cancelled, true otherwise */ protected boolean cancel() { - if (cancelled) { - return false; - } else { - cancelled = true; - return true; - } + return cancelled.compareAndSet(false, true); } /** @@ -557,7 +559,7 @@ public abstract class InterpreterBase extends ParserVisitor { * @return true if cancelled, false otherwise */ protected boolean isCancelled() { - return cancelled | Thread.currentThread().isInterrupted(); + return cancelled.get() | Thread.currentThread().isInterrupted(); } /** diff --git a/src/site/xdoc/changes.xml b/src/site/xdoc/changes.xml index 24e4b2a..7ecbf0c 100644 --- a/src/site/xdoc/changes.xml +++ b/src/site/xdoc/changes.xml @@ -26,6 +26,9 @@ </properties> <body> <release version="3.2" date="unreleased"> + <action dev="henrib" type="add" issue="JEXL-317"> + Support script cancellation through less invasive API + </action> <action dev="henrib" type="fix" issue="JEXL-315" due-to="Mike Bartlett"> JxltEngine literal string strings ending in \ $ or # throw JxltEngine$Exception </action> diff --git a/src/test/java/org/apache/commons/jexl3/ScriptCallableTest.java b/src/test/java/org/apache/commons/jexl3/ScriptCallableTest.java index 313562d..63b5b0d 100644 --- a/src/test/java/org/apache/commons/jexl3/ScriptCallableTest.java +++ b/src/test/java/org/apache/commons/jexl3/ScriptCallableTest.java @@ -27,6 +27,7 @@ import java.util.concurrent.FutureTask; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; import org.apache.commons.jexl3.internal.Script; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -67,7 +68,6 @@ public class ScriptCallableTest extends JexlTestCase { @Test public void testCallableCancel() throws Exception { - List<Runnable> lr = null; final Semaphore latch = new Semaphore(0); JexlContext ctxt = new MapContext(); ctxt.set("latch", latch); @@ -85,6 +85,7 @@ public class ScriptCallableTest extends JexlTestCase { ExecutorService executor = Executors.newFixedThreadPool(2); Future<?> future = executor.submit(c); Future<?> kfc = executor.submit(kc); + List<Runnable> lr; try { Assert.assertTrue((Boolean) kfc.get()); t = future.get(); @@ -98,7 +99,55 @@ public class ScriptCallableTest extends JexlTestCase { Assert.assertTrue(c.isCancelled()); Assert.assertTrue(lr == null || lr.isEmpty()); } + + public static class CancellationContext extends MapContext implements JexlContext.CancellationHandle { + private final AtomicBoolean cancellation; + + CancellationContext(AtomicBoolean c) { + cancellation = c; + } + @Override + public AtomicBoolean getCancellation() { + return cancellation; + } + } + + // JEXL-317 + @Test + public void testCallableCancellation() throws Exception { + final Semaphore latch = new Semaphore(0); + final AtomicBoolean cancel = new AtomicBoolean(false); + JexlContext ctxt = new CancellationContext(cancel); + ctxt.set("latch", latch); + JexlScript e = JEXL.createScript("latch.release(); while(true);"); + final Script.Callable c = (Script.Callable) e.callable(ctxt); + Object t = 42; + Callable<Object> kc = new Callable<Object>() { + @Override + public Object call() throws Exception { + latch.acquire(); + return cancel.compareAndSet(false, true); + } + }; + ExecutorService executor = Executors.newFixedThreadPool(2); + Future<?> future = executor.submit(c); + Future<?> kfc = executor.submit(kc); + List<Runnable> lr; + try { + Assert.assertTrue((Boolean) kfc.get()); + t = future.get(); + Assert.fail("should have been cancelled"); + } catch (ExecutionException xexec) { + // ok, ignore + Assert.assertTrue(xexec.getCause() instanceof JexlException.Cancel); + } finally { + lr = executor.shutdownNow(); + } + Assert.assertTrue(c.isCancelled()); + Assert.assertTrue(lr == null || lr.isEmpty()); + } + @Test public void testCallableTimeout() throws Exception { List<Runnable> lr = null;