diff --git a/build.gradle b/build.gradle index 101f809700..21eabbeeb6 100644 --- a/build.gradle +++ b/build.gradle @@ -204,5 +204,6 @@ test { includeTestsMatching "com.serotonin.mango.rt.maint.work.CreateWorkItemToStringTest" includeTestsMatching "com.serotonin.util.SerializationHelperTest" includeTestsMatching "org.scada_lts.web.mvc.api.json.WorkItemInfoListTest" + includeTestsMatching "org.scada_lts.monitor.ConcurrentMonitoredValuesTest" } } \ No newline at end of file diff --git a/src/com/serotonin/mango/Common.java b/src/com/serotonin/mango/Common.java index a96e4d1af4..183dc05616 100644 --- a/src/com/serotonin/mango/Common.java +++ b/src/com/serotonin/mango/Common.java @@ -29,6 +29,7 @@ import javax.servlet.http.HttpServletRequest; import com.serotonin.mango.web.mvc.controller.ScadaLocaleUtils; +import org.scada_lts.monitor.IMonitoredValues; import gnu.io.CommPortIdentifier; import org.apache.commons.codec.binary.Base64; import org.apache.commons.httpclient.HttpClient; @@ -52,7 +53,7 @@ import com.serotonin.mango.vo.CommPortProxy; import com.serotonin.mango.vo.User; import com.serotonin.mango.web.ContextWrapper; -import com.serotonin.monitor.MonitoredValues; +import org.scada_lts.monitor.ConcurrentMonitoredValues; import com.serotonin.timer.CronTimerTrigger; import com.serotonin.timer.RealTimeTimer; import com.serotonin.util.PropertiesUtils; @@ -76,7 +77,7 @@ public class Common { // This is initialized public static final RealTimeTimer timer = new RealTimeTimer(); - public static final MonitoredValues MONITORED_VALUES = new MonitoredValues(); + public static final IMonitoredValues MONITORED_VALUES = new ConcurrentMonitoredValues(); private static String environmentProfileName = "env"; diff --git a/src/com/serotonin/mango/rt/dataSource/internal/InternalDataSourceRT.java b/src/com/serotonin/mango/rt/dataSource/internal/InternalDataSourceRT.java index 9b305b2f7b..f6c3b47fe7 100644 --- a/src/com/serotonin/mango/rt/dataSource/internal/InternalDataSourceRT.java +++ b/src/com/serotonin/mango/rt/dataSource/internal/InternalDataSourceRT.java @@ -25,7 +25,7 @@ import com.serotonin.mango.rt.dataSource.PollingDataSource; import com.serotonin.mango.vo.dataSource.internal.InternalDataSourceVO; import com.serotonin.mango.vo.dataSource.internal.InternalPointLocatorVO; -import com.serotonin.monitor.IntegerMonitor; +import org.scada_lts.monitor.type.IntegerMonitor; /** * @author Matthew Lohbihler diff --git a/src/com/serotonin/mango/rt/maint/WorkItemMonitor.java b/src/com/serotonin/mango/rt/maint/WorkItemMonitor.java index 28a6e70d59..157fcfea8f 100644 --- a/src/com/serotonin/mango/rt/maint/WorkItemMonitor.java +++ b/src/com/serotonin/mango/rt/maint/WorkItemMonitor.java @@ -4,9 +4,9 @@ import java.util.concurrent.ThreadPoolExecutor; import com.serotonin.mango.Common; -import com.serotonin.monitor.IntegerMonitor; import com.serotonin.timer.FixedRateTrigger; import com.serotonin.timer.TimerTask; +import org.scada_lts.monitor.type.IntegerMonitor; public class WorkItemMonitor extends TimerTask { private static final long TIMEOUT = 1000 * 10; // Run every ten seconds. diff --git a/src/org/scada_lts/mango/service/PointValueService.java b/src/org/scada_lts/mango/service/PointValueService.java index b2f3c654cc..8ef2b2ce0f 100644 --- a/src/org/scada_lts/mango/service/PointValueService.java +++ b/src/org/scada_lts/mango/service/PointValueService.java @@ -49,6 +49,7 @@ import org.scada_lts.dao.pointvalues.PointValueDAO; import org.scada_lts.mango.adapter.MangoPointValues; import org.scada_lts.mango.adapter.MangoPointValuesWithChangeOwner; +import org.scada_lts.monitor.type.IntegerMonitor; import org.springframework.dao.ConcurrencyFailureException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Isolation; @@ -62,7 +63,6 @@ import com.serotonin.mango.rt.maint.work.WorkItem; import com.serotonin.mango.vo.AnonymousUser; import com.serotonin.mango.vo.bean.LongPair; -import com.serotonin.monitor.IntegerMonitor; import com.serotonin.util.queue.ObjectQueue; import static com.serotonin.mango.util.LoggingScriptUtils.infoErrorExecutionScript; diff --git a/src/org/scada_lts/monitor/ConcurrentMonitoredValues.java b/src/org/scada_lts/monitor/ConcurrentMonitoredValues.java new file mode 100644 index 0000000000..02256a0f55 --- /dev/null +++ b/src/org/scada_lts/monitor/ConcurrentMonitoredValues.java @@ -0,0 +1,26 @@ +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by FernFlower decompiler) +// + +package org.scada_lts.monitor; + +import org.scada_lts.monitor.type.ValueMonitor; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class ConcurrentMonitoredValues implements IMonitoredValues { + private final Map> monitors = new ConcurrentHashMap<>(); + + @Override + public ValueMonitor addIfMissingStatMonitor(ValueMonitor monitor) { + monitors.put(monitor.getId(), monitor); + return monitor; + } + + @Override + public ValueMonitor getValueMonitor(String id) { + return monitors.get(id); + } +} diff --git a/src/org/scada_lts/monitor/IMonitoredValues.java b/src/org/scada_lts/monitor/IMonitoredValues.java new file mode 100644 index 0000000000..345a363200 --- /dev/null +++ b/src/org/scada_lts/monitor/IMonitoredValues.java @@ -0,0 +1,8 @@ +package org.scada_lts.monitor; + +import org.scada_lts.monitor.type.ValueMonitor; + +public interface IMonitoredValues { + ValueMonitor addIfMissingStatMonitor(ValueMonitor monitor); + ValueMonitor getValueMonitor(String id); +} diff --git a/src/org/scada_lts/monitor/type/DoubleMonitor.java b/src/org/scada_lts/monitor/type/DoubleMonitor.java new file mode 100644 index 0000000000..fc1db14c0d --- /dev/null +++ b/src/org/scada_lts/monitor/type/DoubleMonitor.java @@ -0,0 +1,33 @@ +package org.scada_lts.monitor.type; + +public class DoubleMonitor extends ValueMonitor { + private double value; + + public DoubleMonitor(String id, String name) { + this(id, name, 0.0); + } + + public DoubleMonitor(String id, String name, double initialValue) { + super(id, name); + this.value = initialValue; + } + + public Double getValue() { + return this.value; + } + + public void setValue(double value) { + this.value = value; + } + + public void addValue(double value) { + this.value += value; + } + + public void setValueIfGreater(double value) { + if (this.value < value) { + this.value = value; + } + + } +} diff --git a/src/org/scada_lts/monitor/type/FloatMonitor.java b/src/org/scada_lts/monitor/type/FloatMonitor.java new file mode 100644 index 0000000000..76e005a2e1 --- /dev/null +++ b/src/org/scada_lts/monitor/type/FloatMonitor.java @@ -0,0 +1,33 @@ +package org.scada_lts.monitor.type; + +public class FloatMonitor extends ValueMonitor { + private float value; + + public FloatMonitor(String id, String name) { + this(id, name, 0.0F); + } + + public FloatMonitor(String id, String name, float initialValue) { + super(id, name); + this.value = initialValue; + } + + public Float getValue() { + return this.value; + } + + public void setValue(float value) { + this.value = value; + } + + public void addValue(float value) { + this.value += value; + } + + public void setValueIfGreater(float value) { + if (this.value < value) { + this.value = value; + } + + } +} diff --git a/src/org/scada_lts/monitor/type/IntegerMonitor.java b/src/org/scada_lts/monitor/type/IntegerMonitor.java new file mode 100644 index 0000000000..c2a0c3497d --- /dev/null +++ b/src/org/scada_lts/monitor/type/IntegerMonitor.java @@ -0,0 +1,33 @@ +package org.scada_lts.monitor.type; + +public class IntegerMonitor extends ValueMonitor { + protected int value; + + public IntegerMonitor(String id, String name) { + this(id, name, 0); + } + + public IntegerMonitor(String id, String name, int initialValue) { + super(id, name); + this.value = initialValue; + } + + public Integer getValue() { + return this.value; + } + + public void setValue(int value) { + this.value = value; + } + + public void addValue(int value) { + this.value += value; + } + + public void setValueIfGreater(int value) { + if (this.value < value) { + this.value = value; + } + + } +} diff --git a/src/org/scada_lts/monitor/type/LongMonitor.java b/src/org/scada_lts/monitor/type/LongMonitor.java new file mode 100644 index 0000000000..820e053513 --- /dev/null +++ b/src/org/scada_lts/monitor/type/LongMonitor.java @@ -0,0 +1,33 @@ +package org.scada_lts.monitor.type; + +public class LongMonitor extends ValueMonitor { + private long value; + + public LongMonitor(String id, String name) { + this(id, name, 0L); + } + + public LongMonitor(String id, String name, long initialValue) { + super(id, name); + this.value = initialValue; + } + + public Long getValue() { + return this.value; + } + + public void setValue(long value) { + this.value = value; + } + + public void addValue(long value) { + this.value += value; + } + + public void setValueIfGreater(long value) { + if (this.value < value) { + this.value = value; + } + + } +} diff --git a/src/org/scada_lts/monitor/type/ObjectMonitor.java b/src/org/scada_lts/monitor/type/ObjectMonitor.java new file mode 100644 index 0000000000..fea2637f52 --- /dev/null +++ b/src/org/scada_lts/monitor/type/ObjectMonitor.java @@ -0,0 +1,26 @@ +package org.scada_lts.monitor.type; + +public class ObjectMonitor extends ValueMonitor { + private T value; + + public ObjectMonitor(String id, String name) { + this(id, name, null); + } + + public ObjectMonitor(String id, String name, T initialValue) { + super(id, name); + this.value = initialValue; + } + + public T getValue() { + return this.value; + } + + public void setValue(T value) { + this.value = value; + } + + public String stringValue() { + return this.value == null ? null : this.value.toString(); + } +} diff --git a/src/org/scada_lts/monitor/type/ValueMonitor.java b/src/org/scada_lts/monitor/type/ValueMonitor.java new file mode 100644 index 0000000000..0edec30507 --- /dev/null +++ b/src/org/scada_lts/monitor/type/ValueMonitor.java @@ -0,0 +1,25 @@ +package org.scada_lts.monitor.type; + + +public abstract class ValueMonitor { + private static final long serialVersionUID = 3992668527962817285L; + private final String id; + private final String name; + + public ValueMonitor(String id, String name) { + this.id = id; + this.name = name; + } + + public String getId() { + return this.id; + } + + public String getName() { + return this.name; + } + + public T getValue() { + throw new UnsupportedOperationException(); + } +} \ No newline at end of file diff --git a/test/org/scada_lts/monitor/ConcurrentMonitoredValuesTest.java b/test/org/scada_lts/monitor/ConcurrentMonitoredValuesTest.java new file mode 100644 index 0000000000..48840271e2 --- /dev/null +++ b/test/org/scada_lts/monitor/ConcurrentMonitoredValuesTest.java @@ -0,0 +1,20 @@ +package org.scada_lts.monitor; + +import org.junit.Test; +import org.scada_lts.monitor.type.ObjectMonitor; +import utils.TestConcurrentUtils; + + +public class ConcurrentMonitoredValuesTest { + + @Test + public void when_addIfMissingStatMonitor_in_ConcurrentMonitoredValues_then_non_exception() throws Throwable { + //given: + IMonitoredValues concurrentMonitoredValues = new ConcurrentMonitoredValues(); + + //when + TestConcurrentUtils.functionCheck(16, concurrentMonitoredValues::addIfMissingStatMonitor, + new ObjectMonitor<>("id", "name")); + } + +} \ No newline at end of file diff --git a/test/utils/MultiThreadEngine.java b/test/utils/MultiThreadEngine.java index 784d9b2b8a..5c1b60614e 100644 --- a/test/utils/MultiThreadEngine.java +++ b/test/utils/MultiThreadEngine.java @@ -4,48 +4,54 @@ import org.slf4j.LoggerFactory; import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.Executor; +import java.util.Set; +import java.util.concurrent.*; public class MultiThreadEngine { - private static Logger LOG = LoggerFactory.getLogger(MultiThreadEngine.class); + private static final Logger LOG = LoggerFactory.getLogger(MultiThreadEngine.class); - public static void execute(final Executor executor, int concurrency, final Runnable action) { + public static void execute(final Executor executor, int concurrency, final Runnable action) throws Throwable { final CountDownLatch ready = new CountDownLatch(concurrency); final CountDownLatch start = new CountDownLatch(1); final CountDownLatch done = new CountDownLatch(concurrency); + final Set throwableMap = new CopyOnWriteArraySet<>(); for (int i = 0; i < concurrency; i++) { executor.execute(() -> { ready.countDown(); try { start.await(); action.run(); - } catch (InterruptedException ex) { - LOG.error(ex.getMessage(), ex); + } catch (Throwable ex) { + throwableMap.add(ex); } finally { done.countDown(); } }); } + long startNanos = 0; try { ready.await(); - long startNanos = System.nanoTime(); + startNanos = System.nanoTime(); start.countDown(); done.await(); - LOG.info("time: {}", (System.nanoTime() - startNanos)/1000000000.0); - } catch (Exception ex) { + if(!throwableMap.isEmpty()) { + throw throwableMap.iterator().next(); + } + } catch (Throwable ex) { LOG.error(ex.getMessage(), ex); + throw ex; + } finally { + LOG.info("time: {}", (System.nanoTime() - startNanos) / 1000000000.0); } } - public static List execute(final Executor executor, int concurrency, final Callable action) { + public static List execute(final Executor executor, int concurrency, final Callable action) throws Throwable { final CountDownLatch ready = new CountDownLatch(concurrency); final CountDownLatch start = new CountDownLatch(1); final CountDownLatch done = new CountDownLatch(concurrency); final List results = new CopyOnWriteArrayList<>(); + final Set throwableMap = new CopyOnWriteArraySet<>(); for (int i = 0; i < concurrency; i++) { executor.execute(() -> { ready.countDown(); @@ -53,29 +59,37 @@ public static List execute(final Executor executor, int concurrency, fina start.await(); R result = action.call(); results.add(result); - } catch (Exception ex) { - LOG.error(ex.getMessage(), ex); + } catch (Throwable ex) { + throwableMap.add(ex); } finally { done.countDown(); } }); } + long startNanos = 0; try { ready.await(); - long startNanos = System.nanoTime(); + startNanos = System.nanoTime(); start.countDown(); done.await(); - LOG.info("time: {}", (System.nanoTime() - startNanos) / 1000000000.0); - } catch (Exception ex) { + + if(!throwableMap.isEmpty()) { + throw throwableMap.iterator().next(); + } + } catch (Throwable ex) { LOG.error(ex.getMessage(), ex); + throw ex; + } finally { + LOG.info("time: {}", (System.nanoTime() - startNanos) / 1000000000.0); } return results; } - public static void execute(final Executor executor, int concurrency, final List actions) { + public static void execute(final Executor executor, int concurrency, final List actions) throws Throwable { final CountDownLatch ready = new CountDownLatch(concurrency * actions.size()); final CountDownLatch start = new CountDownLatch(1); final CountDownLatch done = new CountDownLatch(concurrency * actions.size()); + final Set throwableMap = new CopyOnWriteArraySet<>(); for (int i = 0; i < concurrency; i++) { for (Runnable runnable : actions) { executor.execute(() -> { @@ -83,22 +97,28 @@ public static void execute(final Executor executor, int concurrency, final List< try { start.await(); runnable.run(); - } catch (InterruptedException ex) { - LOG.error(ex.getMessage(), ex); + } catch (Throwable ex) { + throwableMap.add(ex); } finally { done.countDown(); } }); } } + long startNanos = 0; try { ready.await(); - long startNanos = System.nanoTime(); + startNanos = System.nanoTime(); start.countDown(); done.await(); - LOG.info("time: {}", (System.nanoTime() - startNanos)/1000000000.0); - } catch (Exception ex) { + if(!throwableMap.isEmpty()) { + throw throwableMap.iterator().next(); + } + } catch (Throwable ex) { LOG.error(ex.getMessage(), ex); + throw ex; + } finally { + LOG.info("time: {}", (System.nanoTime() - startNanos)/1000000000.0); } } } \ No newline at end of file diff --git a/test/utils/TestConcurrentUtils.java b/test/utils/TestConcurrentUtils.java index d315839db1..520589ebdb 100644 --- a/test/utils/TestConcurrentUtils.java +++ b/test/utils/TestConcurrentUtils.java @@ -11,80 +11,193 @@ public class TestConcurrentUtils { - public static void function(int numberOfLaunches, Function fun, A key) { + public static void function(int numberOfLaunches, Function fun, A key) throws RuntimeException { + try { + functionCheck(numberOfLaunches, fun, key); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + public static void functionCheck(int numberOfLaunches, Function fun, A key) throws Throwable { ExecutorService executor = Executors.newFixedThreadPool(numberOfLaunches); Runnable action = () -> fun.apply(key); - MultiThreadEngine.execute(executor, numberOfLaunches, action); - executor.shutdownNow(); + try { + MultiThreadEngine.execute(executor, numberOfLaunches, action); + } finally { + executor.shutdownNow(); + } + } + + public static void biFunction(int numberOfLaunches, BiFunction fun, A keyA, B keyB) throws RuntimeException { + try { + biFunctionCheck(numberOfLaunches, fun, keyA, keyB); + } catch (Throwable e) { + throw new RuntimeException(e); + } } - public static void biFunction(int numberOfLaunches, BiFunction fun, A keyA, B keyB) { + public static void biFunctionCheck(int numberOfLaunches, BiFunction fun, A keyA, B keyB) throws Throwable { ExecutorService executor = Executors.newFixedThreadPool(numberOfLaunches); Runnable action = () -> fun.apply(keyA, keyB); - MultiThreadEngine.execute(executor, numberOfLaunches, action); - executor.shutdownNow(); + try { + MultiThreadEngine.execute(executor, numberOfLaunches, action); + } finally { + executor.shutdownNow(); + } + } + + public static void biConsumer(int numberOfLaunches, BiConsumer fun, A keyA, B keyB) throws RuntimeException { + try { + biConsumerCheck(numberOfLaunches, fun, keyA, keyB); + } catch (Throwable e) { + throw new RuntimeException(e); + } } - public static void biConsumer(int numberOfLaunches, BiConsumer fun, A keyA, B keyB) { + public static void biConsumerCheck(int numberOfLaunches, BiConsumer fun, A keyA, B keyB) throws Throwable { ExecutorService executor = Executors.newFixedThreadPool(numberOfLaunches); Runnable action = () -> fun.accept(keyA, keyB); - MultiThreadEngine.execute(executor, numberOfLaunches, action); - executor.shutdownNow(); + try { + MultiThreadEngine.execute(executor, numberOfLaunches, action); + } finally { + executor.shutdownNow(); + } + } + + public static void biConsumer(int numberOfLaunches, List actions) throws RuntimeException { + try { + biConsumerCheck(numberOfLaunches, actions); + } catch (Throwable e) { + throw new RuntimeException(e); + } } - public static void biConsumer(int numberOfLaunches, List actions) { + public static void biConsumerCheck(int numberOfLaunches, List actions) throws Throwable { ExecutorService executor = Executors.newFixedThreadPool(numberOfLaunches * actions.size()); List runnables = new ArrayList<>(); for(SupplierVoid action: actions) { runnables.add(action::execute); } - MultiThreadEngine.execute(executor, numberOfLaunches, runnables); - executor.shutdownNow(); + try { + MultiThreadEngine.execute(executor, numberOfLaunches, runnables); + } finally { + executor.shutdownNow(); + } } - public static List functionWithResult(int numberOfLaunches, Function fun, A key) { + public static List functionWithResult(int numberOfLaunches, Function fun, A key) throws RuntimeException { + try { + return functionWithResultCheck(numberOfLaunches, fun, key); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + public static List functionWithResultCheck(int numberOfLaunches, Function fun, A key) throws Throwable { ExecutorService executor = Executors.newFixedThreadPool(numberOfLaunches); Callable action = () -> fun.apply(key); - List result = MultiThreadEngine.execute(executor, numberOfLaunches, action); - executor.shutdownNow(); + List result = null; + try { + result = MultiThreadEngine.execute(executor, numberOfLaunches, action); + } finally { + executor.shutdownNow(); + } return result; } - public static List functionWithResult(int numberOfLaunches, Function fun, A key, Function converter) { + public static List functionWithResult(int numberOfLaunches, Function fun, A key, Function converter) throws RuntimeException { + try { + return functionWithResultCheck(numberOfLaunches, fun, key, converter); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + public static List functionWithResultCheck(int numberOfLaunches, Function fun, A key, Function converter) throws Throwable { ExecutorService executor = Executors.newFixedThreadPool(numberOfLaunches); Callable action = () -> fun.apply(key); - List result = MultiThreadEngine.execute(executor, numberOfLaunches, action); - executor.shutdownNow(); + List result = null; + try { + result = MultiThreadEngine.execute(executor, numberOfLaunches, action); + } finally { + executor.shutdownNow(); + } return result.stream().map(converter).filter(Objects::nonNull).collect(Collectors.toList()); } - public static void consumer(int numberOfLaunches, Consumer fun, A key) { + public static void consumer(int numberOfLaunches, Consumer fun, A key) throws RuntimeException { + try { + consumerCheck(numberOfLaunches, fun, key); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + public static void consumerCheck(int numberOfLaunches, Consumer fun, A key) throws Throwable { ExecutorService executor = Executors.newFixedThreadPool(numberOfLaunches); Runnable action = () -> fun.accept(key); - MultiThreadEngine.execute(executor, numberOfLaunches, action); - executor.shutdownNow(); + try { + MultiThreadEngine.execute(executor, numberOfLaunches, action); + } finally { + executor.shutdownNow(); + } + } + + public static void supplier(int numberOfLaunches, Supplier fun) throws RuntimeException { + try { + supplierCheck(numberOfLaunches, fun); + } catch (Throwable e) { + throw new RuntimeException(e); + } } - public static void supplier(int numberOfLaunches, Supplier fun) { + public static void supplierCheck(int numberOfLaunches, Supplier fun) throws Throwable { ExecutorService executor = Executors.newFixedThreadPool(numberOfLaunches); Runnable action = fun::get; - MultiThreadEngine.execute(executor, numberOfLaunches, action); - executor.shutdownNow(); + try { + MultiThreadEngine.execute(executor, numberOfLaunches, action); + } finally { + executor.shutdownNow(); + } + } + + public static List supplierWithResult(int numberOfLaunches, Supplier fun) throws RuntimeException { + try { + return supplierWithResultCheck(numberOfLaunches, fun); + } catch (Throwable e) { + throw new RuntimeException(e); + } } - public static List supplierWithResult(int numberOfLaunches, Supplier fun) { + public static List supplierWithResultCheck(int numberOfLaunches, Supplier fun) throws Throwable { ExecutorService executor = Executors.newFixedThreadPool(numberOfLaunches); Callable action = fun::get; - List result = MultiThreadEngine.execute(executor, numberOfLaunches, action); - executor.shutdownNow(); + List result = null; + try { + result = MultiThreadEngine.execute(executor, numberOfLaunches, action); + } finally { + executor.shutdownNow(); + } return result; } - public static void supplierVoid(int numberOfLaunches, SupplierVoid fun) { + public static void supplierVoid(int numberOfLaunches, SupplierVoid fun) throws RuntimeException { + try { + supplierVoidCheck(numberOfLaunches, fun); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + public static void supplierVoidCheck(int numberOfLaunches, SupplierVoid fun) throws Throwable { ExecutorService executor = Executors.newFixedThreadPool(numberOfLaunches); Runnable action = fun::execute; - MultiThreadEngine.execute(executor, numberOfLaunches, action); - executor.shutdownNow(); + try { + MultiThreadEngine.execute(executor, numberOfLaunches, action); + } finally { + executor.shutdownNow(); + } } @FunctionalInterface