View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.jexl3;
18  
19  import static org.junit.jupiter.api.Assertions.assertEquals;
20  import static org.junit.jupiter.api.Assertions.assertFalse;
21  import static org.junit.jupiter.api.Assertions.assertNotEquals;
22  import static org.junit.jupiter.api.Assertions.assertThrows;
23  import static org.junit.jupiter.api.Assertions.assertTrue;
24  
25  import java.time.Duration;
26  import java.util.List;
27  import java.util.concurrent.Callable;
28  import java.util.concurrent.CancellationException;
29  import java.util.concurrent.ExecutionException;
30  import java.util.concurrent.ExecutorService;
31  import java.util.concurrent.Executors;
32  import java.util.concurrent.Future;
33  import java.util.concurrent.FutureTask;
34  import java.util.concurrent.Semaphore;
35  import java.util.concurrent.TimeUnit;
36  import java.util.concurrent.TimeoutException;
37  import java.util.concurrent.atomic.AtomicBoolean;
38  
39  import org.apache.commons.jexl3.internal.Script;
40  import org.apache.commons.lang3.ThreadUtils;
41  import org.junit.jupiter.api.Test;
42  
43  /**
44   * Tests around asynchronous script execution and interrupts.
45   */
46  @SuppressWarnings({ "UnnecessaryBoxing", "AssertEqualsBetweenInconvertibleTypes" })
47  class ScriptCallableTest extends JexlTestCase {
48      public static class AnnotationContext extends MapContext implements JexlContext.AnnotationProcessor {
49          @Override
50          public Object processAnnotation(final String name, final Object[] args, final Callable<Object> statement) throws Exception {
51              if ("timeout".equals(name) && args != null && args.length > 0) {
52                  final long ms = args[0] instanceof Number ? ((Number) args[0]).longValue() : Long.parseLong(args[0].toString());
53                  final Object def = args.length > 1 ? args[1] : null;
54                  if (ms > 0) {
55                      final ExecutorService executor = Executors.newFixedThreadPool(1);
56                      Future<?> future = null;
57                      try {
58                          future = executor.submit(statement);
59                          return future.get(ms, TimeUnit.MILLISECONDS);
60                      } catch (final TimeoutException xtimeout) {
61                          future.cancel(true);
62                      } finally {
63                          executor.shutdown();
64                      }
65  
66                  }
67                  return def;
68              }
69              return statement.call();
70          }
71  
72          public void sleep(final long ms) throws InterruptedException {
73              Thread.sleep(ms);
74          }
75  
76      }
77  
78      public static class CancellationContext extends MapContext implements JexlContext.CancellationHandle {
79          private final AtomicBoolean cancellation;
80  
81          CancellationContext(final AtomicBoolean c) {
82              cancellation = c;
83          }
84  
85          @Override
86          public AtomicBoolean getCancellation() {
87              return cancellation;
88          }
89      }
90  
91      public static class TestContext extends MapContext implements JexlContext.NamespaceResolver {
92          public int hangs(final Object t) {
93              return 1;
94          }
95  
96          public int interrupt() {
97              Thread.currentThread().interrupt();
98              return 42;
99          }
100 
101         @Override
102         public Object resolveNamespace(final String name) {
103             return name == null ? this : null;
104         }
105 
106         public int runForever() {
107             while (true) {
108                 if (Thread.currentThread().isInterrupted()) {
109                     break;
110                 }
111             }
112             return 1;
113         }
114 
115         public void sleep(final long millis) throws InterruptedException {
116             Thread.sleep(millis);
117         }
118 
119         public int wait(final int s) throws InterruptedException {
120             Thread.sleep(1000 * s);
121             return s;
122         }
123 
124         public int waitInterrupt(final int s) {
125             try {
126                 Thread.sleep(1000 * s);
127                 return s;
128             } catch (final InterruptedException xint) {
129                 Thread.currentThread().interrupt();
130             }
131             return -1;
132         }
133     }
134 
135     // private Log logger = LogFactory.getLog(JexlEngine.class);
136     public ScriptCallableTest() {
137         super("ScriptCallableTest");
138     }
139 
140     /**
141      * Redundant test with previous ones but impervious to JEXL engine configuration.
142      *
143      * @throws Exception Thrown if there is a regression
144      */
145     private void runInterrupt(final JexlEngine jexl) throws Exception {
146         List<Runnable> lr = null;
147         final ExecutorService executorService = Executors.newFixedThreadPool(2);
148         try {
149             final JexlContext ctxt = new TestContext();
150 
151             // run an interrupt
152             final JexlScript sint = jexl.createScript("interrupt(); return 42");
153             Object t = null;
154             Script.Callable c = (Script.Callable) sint.callable(ctxt);
155             try {
156                 t = c.call();
157                 assertFalse(c.isCancellable(), "should have thrown a Cancel");
158             } catch (final JexlException.Cancel xjexl) {
159                 assertTrue(c.isCancellable(), () -> "should not have thrown " + xjexl);
160             }
161             assertTrue(c.isCancelled());
162             assertNotEquals(42, t);
163 
164             // self interrupt
165             c = (Script.Callable) sint.callable(ctxt);
166             final Future<Object> f = executorService.submit(c);
167             try {
168                 t = f.get();
169                 assertFalse(c.isCancellable(), "should have thrown a Cancel");
170             } catch (final ExecutionException xexec) {
171                 assertTrue(c.isCancellable(), () -> "should not have thrown " + xexec);
172             }
173             assertTrue(c.isCancelled());
174             assertNotEquals(42, t);
175 
176             // timeout a sleep
177             final JexlScript ssleep = jexl.createScript("sleep(30000); return 42");
178             final Future<Object> f0 = executorService.submit(ssleep.callable(ctxt));
179             assertThrows(TimeoutException.class, () -> f0.get(100L, TimeUnit.MILLISECONDS));
180             f0.cancel(true);
181             assertNotEquals(42, t);
182 
183             // cancel a sleep
184             final Future<Object> fc0 = executorService.submit(ssleep.callable(ctxt));
185             final Runnable cancels0 = () -> {
186                 ThreadUtils.sleepQuietly(Duration.ofMillis(200));
187                 fc0.cancel(true);
188             };
189             executorService.submit(cancels0);
190             assertThrows(CancellationException.class, () -> f0.get(100L, TimeUnit.MILLISECONDS));
191 
192             // timeout a while(true)
193             final JexlScript swhile = jexl.createScript("while(true); return 42");
194             final Future<Object> f1 = executorService.submit(swhile.callable(ctxt));
195             assertThrows(TimeoutException.class, () -> f1.get(100L, TimeUnit.MILLISECONDS));
196             f1.cancel(true);
197 
198             assertNotEquals(42, t);
199 
200             // cancel a while(true)
201             final Future<Object> fc = executorService.submit(swhile.callable(ctxt));
202             final Runnable cancels = () -> {
203                 ThreadUtils.sleepQuietly(Duration.ofMillis(200));
204                 fc.cancel(true);
205             };
206             executorService.submit(cancels);
207             assertThrows(CancellationException.class, fc::get);
208             assertNotEquals(42, t);
209         } finally {
210             lr = executorService.shutdownNow();
211         }
212         assertTrue(lr.isEmpty());
213     }
214 
215     @Test
216     void testCallableCancel() throws Exception {
217         final Semaphore latch = new Semaphore(0);
218         final JexlContext ctxt = new MapContext();
219         ctxt.set("latch", latch);
220 
221         final JexlScript e = JEXL.createScript("latch.release(); while(true);");
222         final Script.Callable c = (Script.Callable) e.callable(ctxt);
223         final Callable<Object> kc = () -> {
224             latch.acquire();
225             return c.cancel();
226         };
227         final List<Runnable> list;
228         final ExecutorService executor = Executors.newFixedThreadPool(2);
229         try {
230             final Future<?> future = executor.submit(c);
231             final Future<?> kfc = executor.submit(kc);
232             assertTrue((Boolean) kfc.get());
233             final ExecutionException xexec = assertThrows(ExecutionException.class, future::get);
234             assertTrue(xexec.getCause() instanceof JexlException.Cancel);
235         } finally {
236             list = executor.shutdownNow();
237         }
238         assertTrue(c.isCancelled());
239         assertTrue(list == null || list.isEmpty());
240     }
241 
242     /**
243      * Tests JEXL-317.
244      */
245     @Test
246     void testCallableCancellation() throws Exception {
247         final Semaphore latch = new Semaphore(0);
248         final AtomicBoolean cancel = new AtomicBoolean();
249         final JexlContext ctxt = new CancellationContext(cancel);
250         ctxt.set("latch", latch);
251 
252         final JexlScript e = JEXL.createScript("latch.release(); while(true);");
253         final Script.Callable c = (Script.Callable) e.callable(ctxt);
254         final Callable<Object> kc = () -> {
255             latch.acquire();
256             return cancel.compareAndSet(false, true);
257         };
258         final List<Runnable> list;
259         final ExecutorService executor = Executors.newFixedThreadPool(2);
260         try {
261             final Future<?> future = executor.submit(c);
262             final Future<?> kfc = executor.submit(kc);
263             assertTrue((Boolean) kfc.get());
264             final ExecutionException xexec = assertThrows(ExecutionException.class, future::get);
265             assertTrue(xexec.getCause() instanceof JexlException.Cancel);
266         } finally {
267             list = executor.shutdownNow();
268         }
269         assertTrue(c.isCancelled());
270         assertTrue(list == null || list.isEmpty());
271     }
272 
273     @Test
274     void testCallableClosure() throws Exception {
275         final JexlScript e = JEXL.createScript("function(t) {while(t);}");
276         final Callable<Object> c = e.callable(null, Boolean.TRUE);
277         final List<Runnable> list;
278         final ExecutorService executor = Executors.newFixedThreadPool(1);
279         final Future<?> future;
280         try {
281             future = executor.submit(c);
282             assertThrows(TimeoutException.class, () -> future.get(100, TimeUnit.MILLISECONDS));
283             future.cancel(true);
284         } finally {
285             list = executor.shutdownNow();
286         }
287         assertTrue(future.isCancelled());
288         assertTrue(list.isEmpty());
289     }
290 
291     @Test
292     void testCallableTimeout() throws Exception {
293         final Semaphore latch = new Semaphore(0);
294         final JexlContext ctxt = new MapContext();
295         ctxt.set("latch", latch);
296         final JexlScript e = JEXL.createScript("latch.release(); while(true);");
297         final Callable<Object> c = e.callable(ctxt);
298         final List<Runnable> list;
299         final ExecutorService executor = Executors.newFixedThreadPool(1);
300         final Future<?> future;
301         try {
302             future = executor.submit(c);
303             latch.acquire();
304             assertThrows(TimeoutException.class, () -> future.get(100, TimeUnit.MILLISECONDS));
305             future.cancel(true);
306         } finally {
307             list = executor.shutdownNow();
308         }
309         assertTrue(future.isCancelled());
310         assertTrue(list.isEmpty());
311     }
312 
313     @Test
314     void testCancelForever() throws Exception {
315         final Semaphore latch = new Semaphore(0);
316         final JexlContext ctxt = new TestContext();
317         ctxt.set("latch", latch);
318 
319         final JexlScript e = JEXL.createScript("latch.release(); runForever()");
320         final Callable<Object> c = e.callable(ctxt);
321 
322         final List<Runnable> list;
323         final ExecutorService executor = Executors.newFixedThreadPool(1);
324         final Future<?> future;
325         try {
326             future = executor.submit(c);
327             assertThrows(TimeoutException.class, () -> future.get(100, TimeUnit.MILLISECONDS));
328             future.cancel(true);
329         } finally {
330             list = executor.shutdownNow();
331         }
332         assertTrue(future.isCancelled());
333         assertTrue(list.isEmpty());
334     }
335 
336     @Test
337     void testCancelLoopWait() throws Exception {
338         final List<Runnable> list;
339         final JexlScript e = JEXL.createScript("while (true) { wait(10) }");
340         final Callable<Object> c = e.callable(new TestContext());
341         final ExecutorService executor = Executors.newFixedThreadPool(1);
342         final Future<?> future;
343         try {
344             future = executor.submit(c);
345             assertThrows(TimeoutException.class, () -> future.get(100, TimeUnit.MILLISECONDS));
346             future.cancel(true);
347         } finally {
348             list = executor.shutdownNow();
349         }
350         assertTrue(future.isCancelled());
351         assertTrue(list.isEmpty());
352     }
353 
354     @Test
355     void testCancelWait() throws Exception {
356         final List<Runnable> list;
357         final JexlScript e = JEXL.createScript("wait(10)");
358         final Callable<Object> c = e.callable(new TestContext());
359         final ExecutorService executor = Executors.newFixedThreadPool(1);
360         try {
361             final Future<?> future = executor.submit(c);
362             assertThrows(TimeoutException.class, () -> future.get(100, TimeUnit.MILLISECONDS));
363             future.cancel(true);
364             assertTrue(future.isCancelled());
365         } finally {
366             list = executor.shutdownNow();
367         }
368         assertTrue(list.isEmpty());
369     }
370 
371     @Test
372     void testCancelWaitInterrupt() throws Exception {
373         final List<Runnable> list;
374         final JexlScript e = JEXL.createScript("waitInterrupt(42)");
375         final Callable<Object> c = e.callable(new TestContext());
376         final ExecutorService executor = Executors.newFixedThreadPool(1);
377         final Future<?> future;
378         try {
379             future = executor.submit(c);
380             assertThrows(TimeoutException.class, () -> future.get(100, TimeUnit.MILLISECONDS));
381             future.cancel(true);
382         } finally {
383             list = executor.shutdownNow();
384         }
385         assertTrue(future.isCancelled());
386         assertTrue(list.isEmpty());
387     }
388 
389     @Test
390     void testFuture() throws Exception {
391         final JexlScript e = JEXL.createScript("while(true);");
392         final FutureTask<Object> future = new FutureTask<>(e.callable(null));
393         final ExecutorService executor = Executors.newFixedThreadPool(1);
394         try {
395             executor.submit(future);
396             assertThrows(TimeoutException.class, () -> future.get(100, TimeUnit.MILLISECONDS));
397             future.cancel(true);
398         } finally {
399             executor.shutdown();
400         }
401         assertTrue(future.isCancelled());
402     }
403 
404     @Test
405     void testHangs() throws Exception {
406         final JexlScript e = JEXL.createScript("hangs()");
407         final Callable<Object> c = e.callable(new TestContext());
408         final ExecutorService executor = Executors.newFixedThreadPool(1);
409         try {
410             final Future<?> future = executor.submit(c);
411             final ExecutionException xexec = assertThrows(ExecutionException.class, () -> future.get(1, TimeUnit.SECONDS));
412             assertTrue(xexec.getCause() instanceof JexlException.Method);
413         } finally {
414             executor.shutdown();
415         }
416     }
417 
418     @Test
419     void testInterruptCancellable() throws Exception {
420         runInterrupt(new JexlBuilder().silent(true).strict(true).cancellable(true).create());
421     }
422 
423     @Test
424     void testInterruptSilentLenient() throws Exception {
425         runInterrupt(new JexlBuilder().silent(true).strict(false).create());
426     }
427 
428     @Test
429     void testInterruptSilentStrict() throws Exception {
430         runInterrupt(new JexlBuilder().silent(true).strict(true).create());
431     }
432 
433     @Test
434     void testInterruptVerboseLenient() throws Exception {
435         runInterrupt(new JexlBuilder().silent(false).strict(false).create());
436     }
437 
438     @Test
439     void testInterruptVerboseStrict() throws Exception {
440         runInterrupt(new JexlBuilder().silent(false).strict(true).create());
441     }
442 
443     @Test
444     void testNoWait() throws Exception {
445         List<Runnable> lr = null;
446         final JexlScript e = JEXL.createScript("wait(0)");
447         final Callable<Object> c = e.callable(new TestContext());
448 
449         final ExecutorService executor = Executors.newFixedThreadPool(1);
450         try {
451             final Future<?> future = executor.submit(c);
452             final Object t = future.get(2, TimeUnit.SECONDS);
453             assertTrue(future.isDone());
454             assertEquals(0, t);
455         } finally {
456             lr = executor.shutdownNow();
457         }
458         assertTrue(lr.isEmpty());
459     }
460 
461     @Test
462     void testTimeout() throws Exception {
463         JexlScript script = JEXL.createScript("(flag)->{ @timeout(100) { while(flag); return 42 }; 'cancelled' }");
464         final JexlContext ctxt = new AnnotationContext();
465         Object result = script.execute(ctxt, true);
466         assertEquals("cancelled", result);
467 
468         result = script.execute(ctxt, false);
469         assertEquals(42, result);
470         script = JEXL.createScript("(flag)->{ @timeout(100, 'cancelled') { while(flag); 42; } }");
471         result = script.execute(ctxt, true);
472         assertEquals("cancelled", result);
473 
474         result = script.execute(ctxt, false);
475         assertEquals(42, result);
476         script = JEXL.createScript("@timeout(100) {sleep(1000); 42; } -42;");
477         result = script.execute(ctxt);
478         assertEquals(-42, result);
479 
480         script = JEXL.createScript("@timeout(100) {sleep(1000); return 42; } return -42;");
481         result = script.execute(ctxt);
482         assertEquals(-42, result);
483         script = JEXL.createScript("@timeout(1000) {sleep(100); return 42; } return -42;");
484         result = script.execute(ctxt);
485         assertEquals(42, result);
486     }
487 
488     @Test
489     void testWait() throws Exception {
490         List<Runnable> lr = null;
491         final JexlScript e = JEXL.createScript("wait(1)");
492         final Callable<Object> c = e.callable(new TestContext());
493 
494         final ExecutorService executor = Executors.newFixedThreadPool(1);
495         try {
496             final Future<?> future = executor.submit(c);
497             final Object t = future.get(2, TimeUnit.SECONDS);
498             assertEquals(1, t);
499         } finally {
500             lr = executor.shutdownNow();
501         }
502         assertTrue(lr.isEmpty());
503     }
504 }