blob: 671f1e631b695306ee3405cbcca2101a751ac16f [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.jexl3;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
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.junit.Assert;
import org.junit.Test;
/**
* Tests around asynchronous script execution and interrupts.
*/
@SuppressWarnings({"UnnecessaryBoxing", "AssertEqualsBetweenInconvertibleTypes"})
public class ScriptCallableTest extends JexlTestCase {
//private Log logger = LogFactory.getLog(JexlEngine.class);
public ScriptCallableTest() {
super("ScriptCallableTest");
}
@Test
public void testFuture() throws Exception {
final JexlScript e = JEXL.createScript("while(true);");
final FutureTask<Object> future = new FutureTask<Object>(e.callable(null));
final ExecutorService executor = Executors.newFixedThreadPool(1);
executor.submit(future);
Object t = 42;
try {
t = future.get(100, TimeUnit.MILLISECONDS);
Assert.fail("should have timed out");
} catch (final TimeoutException xtimeout) {
// ok, ignore
future.cancel(true);
} finally {
executor.shutdown();
}
Assert.assertTrue(future.isCancelled());
Assert.assertEquals(42, t);
}
@Test
public void testCallableCancel() throws Exception {
final Semaphore latch = new Semaphore(0);
final JexlContext ctxt = new MapContext();
ctxt.set("latch", latch);
final JexlScript e = JEXL.createScript("latch.release(); while(true);");
final Script.Callable c = (Script.Callable) e.callable(ctxt);
Object t = 42;
final Callable<Object> kc = () -> {
latch.acquire();
return c.cancel();
};
final ExecutorService executor = Executors.newFixedThreadPool(2);
final Future<?> future = executor.submit(c);
final Future<?> kfc = executor.submit(kc);
List<Runnable> lr;
try {
Assert.assertTrue((Boolean) kfc.get());
t = future.get();
Assert.fail("should have been cancelled");
} catch (final 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());
}
public static class CancellationContext extends MapContext implements JexlContext.CancellationHandle {
private final AtomicBoolean cancellation;
CancellationContext(final 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);
final JexlContext ctxt = new CancellationContext(cancel);
ctxt.set("latch", latch);
final JexlScript e = JEXL.createScript("latch.release(); while(true);");
final Script.Callable c = (Script.Callable) e.callable(ctxt);
Object t = 42;
final Callable<Object> kc = () -> {
latch.acquire();
return cancel.compareAndSet(false, true);
};
final ExecutorService executor = Executors.newFixedThreadPool(2);
final Future<?> future = executor.submit(c);
final Future<?> kfc = executor.submit(kc);
List<Runnable> lr;
try {
Assert.assertTrue((Boolean) kfc.get());
t = future.get();
Assert.fail("should have been cancelled");
} catch (final 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;
final Semaphore latch = new Semaphore(0);
final JexlContext ctxt = new MapContext();
ctxt.set("latch", latch);
final JexlScript e = JEXL.createScript("latch.release(); while(true);");
final Callable<Object> c = e.callable(ctxt);
Object t = 42;
final ExecutorService executor = Executors.newFixedThreadPool(1);
final Future<?> future = executor.submit(c);
try {
latch.acquire();
t = future.get(100, TimeUnit.MILLISECONDS);
Assert.fail("should have timed out");
} catch (final TimeoutException xtimeout) {
// ok, ignore
future.cancel(true);
} finally {
lr = executor.shutdownNow();
}
Assert.assertTrue(future.isCancelled());
Assert.assertEquals(42, t);
Assert.assertTrue(lr.isEmpty());
}
@Test
public void testCallableClosure() throws Exception {
List<Runnable> lr = null;
final JexlScript e = JEXL.createScript("function(t) {while(t);}");
final Callable<Object> c = e.callable(null, Boolean.TRUE);
Object t = 42;
final ExecutorService executor = Executors.newFixedThreadPool(1);
final Future<?> future = executor.submit(c);
try {
t = future.get(100, TimeUnit.MILLISECONDS);
Assert.fail("should have timed out");
} catch (final TimeoutException xtimeout) {
// ok, ignore
future.cancel(true);
} finally {
lr = executor.shutdownNow();
}
Assert.assertTrue(future.isCancelled());
Assert.assertEquals(42, t);
Assert.assertTrue(lr.isEmpty());
}
public static class TestContext extends MapContext implements JexlContext.NamespaceResolver {
@Override
public Object resolveNamespace(final String name) {
return name == null ? this : null;
}
public int wait(final int s) throws InterruptedException {
Thread.sleep(1000 * s);
return s;
}
public int waitInterrupt(final int s) {
try {
Thread.sleep(1000 * s);
return s;
} catch (final InterruptedException xint) {
Thread.currentThread().interrupt();
}
return -1;
}
public int runForever() {
while (true) {
if (Thread.currentThread().isInterrupted()) {
break;
}
}
return 1;
}
public int interrupt() throws InterruptedException {
Thread.currentThread().interrupt();
return 42;
}
public void sleep(final long millis) throws InterruptedException {
Thread.sleep(millis);
}
public int hangs(final Object t) {
return 1;
}
}
@Test
public void testNoWait() throws Exception {
List<Runnable> lr = null;
final JexlScript e = JEXL.createScript("wait(0)");
final Callable<Object> c = e.callable(new TestContext());
final ExecutorService executor = Executors.newFixedThreadPool(1);
try {
final Future<?> future = executor.submit(c);
final Object t = future.get(2, TimeUnit.SECONDS);
Assert.assertTrue(future.isDone());
Assert.assertEquals(0, t);
} finally {
lr = executor.shutdownNow();
}
Assert.assertTrue(lr.isEmpty());
}
@Test
public void testWait() throws Exception {
List<Runnable> lr = null;
final JexlScript e = JEXL.createScript("wait(1)");
final Callable<Object> c = e.callable(new TestContext());
final ExecutorService executor = Executors.newFixedThreadPool(1);
try {
final Future<?> future = executor.submit(c);
final Object t = future.get(2, TimeUnit.SECONDS);
Assert.assertEquals(1, t);
} finally {
lr = executor.shutdownNow();
}
Assert.assertTrue(lr.isEmpty());
}
@Test
public void testCancelWait() throws Exception {
List<Runnable> lr = null;
final JexlScript e = JEXL.createScript("wait(10)");
final Callable<Object> c = e.callable(new TestContext());
final ExecutorService executor = Executors.newFixedThreadPool(1);
try {
final Future<?> future = executor.submit(c);
Object t = 42;
try {
t = future.get(100, TimeUnit.MILLISECONDS);
Assert.fail("should have timed out");
} catch (final TimeoutException xtimeout) {
// ok, ignore
future.cancel(true);
}
Assert.assertTrue(future.isCancelled());
Assert.assertEquals(42, t);
} finally {
lr = executor.shutdownNow();
}
Assert.assertTrue(lr.isEmpty());
}
@Test
public void testCancelWaitInterrupt() throws Exception {
List<Runnable> lr = null;
final JexlScript e = JEXL.createScript("waitInterrupt(42)");
final Callable<Object> c = e.callable(new TestContext());
final ExecutorService executor = Executors.newFixedThreadPool(1);
final Future<?> future = executor.submit(c);
Object t = 42;
try {
t = future.get(100, TimeUnit.MILLISECONDS);
Assert.fail("should have timed out");
} catch (final TimeoutException xtimeout) {
// ok, ignore
future.cancel(true);
} finally {
lr = executor.shutdownNow();
}
Assert.assertTrue(future.isCancelled());
Assert.assertEquals(42, t);
Assert.assertTrue(lr.isEmpty());
}
@Test
public void testCancelForever() throws Exception {
List<Runnable> lr = null;
final Semaphore latch = new Semaphore(0);
final JexlContext ctxt = new TestContext();
ctxt.set("latch", latch);
final JexlScript e = JEXL.createScript("latch.release(); runForever()");
final Callable<Object> c = e.callable(ctxt);
final ExecutorService executor = Executors.newFixedThreadPool(1);
final Future<?> future = executor.submit(c);
Object t = 42;
try {
latch.acquire();
t = future.get(100, TimeUnit.MILLISECONDS);
Assert.fail("should have timed out");
} catch (final TimeoutException xtimeout) {
// ok, ignore
future.cancel(true);
} finally {
lr = executor.shutdownNow();
}
Assert.assertTrue(future.isCancelled());
Assert.assertEquals(42, t);
Assert.assertTrue(lr.isEmpty());
}
@Test
public void testCancelLoopWait() throws Exception {
List<Runnable> lr = null;
final JexlScript e = JEXL.createScript("while (true) { wait(10) }");
final Callable<Object> c = e.callable(new TestContext());
final ExecutorService executor = Executors.newFixedThreadPool(1);
final Future<?> future = executor.submit(c);
Object t = 42;
try {
t = future.get(100, TimeUnit.MILLISECONDS);
Assert.fail("should have timed out");
} catch (final TimeoutException xtimeout) {
future.cancel(true);
} finally {
lr = executor.shutdownNow();
}
Assert.assertTrue(future.isCancelled());
Assert.assertEquals(42, t);
Assert.assertTrue(lr.isEmpty());
}
@Test
public void testInterruptVerboseStrict() throws Exception {
runInterrupt(new JexlBuilder().silent(false).strict(true).create());
}
@Test
public void testInterruptVerboseLenient() throws Exception {
runInterrupt(new JexlBuilder().silent(false).strict(false).create());
}
@Test
public void testInterruptSilentStrict() throws Exception {
runInterrupt(new JexlBuilder().silent(true).strict(true).create());
}
@Test
public void testInterruptSilentLenient() throws Exception {
runInterrupt(new JexlBuilder().silent(true).strict(false).create());
}
@Test
public void testInterruptCancellable() throws Exception {
runInterrupt(new JexlBuilder().silent(true).strict(true).cancellable(true).create());
}
/**
* Redundant test with previous ones but impervious to JEXL engine configuation.
* @throws Exception if there is a regression
*/
private void runInterrupt(final JexlEngine jexl) throws Exception {
List<Runnable> lr = null;
final ExecutorService exec = Executors.newFixedThreadPool(2);
try {
final JexlContext ctxt = new TestContext();
// run an interrupt
final JexlScript sint = jexl.createScript("interrupt(); return 42");
Object t = null;
Script.Callable c = (Script.Callable) sint.callable(ctxt);
try {
t = c.call();
if (c.isCancellable()) {
Assert.fail("should have thrown a Cancel");
}
} catch (final JexlException.Cancel xjexl) {
if (!c.isCancellable()) {
Assert.fail("should not have thrown " + xjexl);
}
}
Assert.assertTrue(c.isCancelled());
Assert.assertNotEquals(42, t);
// self interrupt
Future<Object> f = null;
c = (Script.Callable) sint.callable(ctxt);
try {
f = exec.submit(c);
t = f.get();
if (c.isCancellable()) {
Assert.fail("should have thrown a Cancel");
}
} catch (final ExecutionException xexec) {
if (!c.isCancellable()) {
Assert.fail("should not have thrown " + xexec);
}
}
Assert.assertTrue(c.isCancelled());
Assert.assertNotEquals(42, t);
// timeout a sleep
final JexlScript ssleep = jexl.createScript("sleep(30000); return 42");
try {
f = exec.submit(ssleep.callable(ctxt));
t = f.get(100L, TimeUnit.MILLISECONDS);
Assert.fail("should timeout");
} catch (final TimeoutException xtimeout) {
if (f != null) {
f.cancel(true);
}
}
Assert.assertNotEquals(42, t);
// cancel a sleep
try {
final Future<Object> fc = exec.submit(ssleep.callable(ctxt));
final Runnable cancels = () -> {
try {
Thread.sleep(200L);
} catch (final Exception xignore) {
}
fc.cancel(true);
};
exec.submit(cancels);
t = f.get(100L, TimeUnit.MILLISECONDS);
Assert.fail("should be cancelled");
} catch (final CancellationException xexec) {
// this is the expected result
}
// timeout a while(true)
final JexlScript swhile = jexl.createScript("while(true); return 42");
try {
f = exec.submit(swhile.callable(ctxt));
t = f.get(100L, TimeUnit.MILLISECONDS);
Assert.fail("should timeout");
} catch (final TimeoutException xtimeout) {
if (f != null) {
f.cancel(true);
}
}
Assert.assertNotEquals(42, t);
// cancel a while(true)
try {
final Future<Object> fc = exec.submit(swhile.callable(ctxt));
final Runnable cancels = () -> {
try {
Thread.sleep(200L);
} catch (final Exception xignore) {
}
fc.cancel(true);
};
exec.submit(cancels);
t = fc.get();
Assert.fail("should be cancelled");
} catch (final CancellationException xexec) {
// this is the expected result
}
Assert.assertNotEquals(42, t);
} finally {
lr = exec.shutdownNow();
}
Assert.assertTrue(lr.isEmpty());
}
@Test
public void testHangs() throws Exception {
final JexlScript e = JEXL.createScript("hangs()");
final Callable<Object> c = e.callable(new TestContext());
final ExecutorService executor = Executors.newFixedThreadPool(1);
try {
final Future<?> future = executor.submit(c);
final Object t = future.get(1, TimeUnit.SECONDS);
Assert.fail("hangs should not be solved");
} catch(final ExecutionException xexec) {
Assert.assertTrue(xexec.getCause() instanceof JexlException.Method);
} finally {
executor.shutdown();
}
}
public static class AnnotationContext extends MapContext implements JexlContext.AnnotationProcessor {
@Override
public Object processAnnotation(final String name, final Object[] args, final Callable<Object> statement) throws Exception {
if ("timeout".equals(name) && args != null && args.length > 0) {
final long ms = args[0] instanceof Number
? ((Number) args[0]).longValue()
: Long.parseLong(args[0].toString());
final Object def = args.length > 1? args[1] : null;
if (ms > 0) {
final ExecutorService executor = Executors.newFixedThreadPool(1);
Future<?> future = null;
try {
future = executor.submit(statement);
return future.get(ms, TimeUnit.MILLISECONDS);
} catch (final TimeoutException xtimeout) {
if (future != null) {
future.cancel(true);
}
} finally {
executor.shutdown();
}
}
return def;
}
return statement.call();
}
public void sleep(final long ms) throws InterruptedException {
Thread.sleep(ms);
}
}
@Test
public void testTimeout() throws Exception {
JexlScript script = JEXL.createScript("(flag)->{ @timeout(100) { while(flag); return 42 }; 'cancelled' }");
final JexlContext ctxt = new AnnotationContext();
Object result = null;
try {
result = script.execute(ctxt, true);
} catch (final Exception xany) {
Assert.fail(xany.toString());
}
Assert.assertEquals("cancelled", result);
result = script.execute(ctxt, false);
Assert.assertEquals(42, result);
script = JEXL.createScript("(flag)->{ @timeout(100, 'cancelled') { while(flag); 42; } }");
try {
result = script.execute(ctxt, true);
} catch (final Exception xany) {
Assert.fail(xany.toString());
}
Assert.assertEquals("cancelled", result);
result = script.execute(ctxt, false);
Assert.assertEquals(42, result);
script = JEXL.createScript("@timeout(10) {sleep(1000); 42; } -42;");
try {
result = script.execute(ctxt);
} catch (final Exception xany) {
Assert.fail(xany.toString());
}
Assert.assertEquals(-42, result);
script = JEXL.createScript("@timeout(10) {sleep(1000); return 42; } return -42;");
try {
result = script.execute(ctxt);
} catch (final Exception xany) {
Assert.fail(xany.toString());
}
Assert.assertEquals(-42, result);
script = JEXL.createScript("@timeout(1000) {sleep(10); return 42; } return -42;");
try {
result = script.execute(ctxt);
} catch (final Exception xany) {
Assert.fail(xany.toString());
}
Assert.assertEquals(42, result);
}
}