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;

Reply via email to