Commit 831f6201dbf5d4d85c6d38f9a47de05a50e5ac8e

Authored by vparomskiy
1 parent 4ba34513

sync deduplicated script evaluation/release

... ... @@ -154,7 +154,7 @@ class DefaultTbContext implements TbContext {
154 154
155 155 @Override
156 156 public ScriptEngine createJsScriptEngine(String script, String... argNames) {
157   - return new RuleNodeJsScriptEngine(mainCtx.getJsSandbox(), script, argNames);
  157 + return new RuleNodeJsScriptEngine(mainCtx.getJsSandbox(), nodeCtx.getSelf().getId(), script, argNames);
158 158 }
159 159
160 160 @Override
... ...
... ... @@ -276,7 +276,7 @@ public class RuleChainController extends BaseController {
276 276 String errorText = "";
277 277 ScriptEngine engine = null;
278 278 try {
279   - engine = new RuleNodeJsScriptEngine(jsSandboxService, script, argNames);
  279 + engine = new RuleNodeJsScriptEngine(jsSandboxService, getCurrentUser().getId(), script, argNames);
280 280 TbMsg inMsg = new TbMsg(UUIDs.timeBased(), msgType, null, new TbMsgMetaData(metadata), data, null, null, 0L);
281 281 switch (scriptType) {
282 282 case "update":
... ...
... ... @@ -13,16 +13,19 @@
13 13 * See the License for the specific language governing permissions and
14 14 * limitations under the License.
15 15 */
16   -
17 16 package org.thingsboard.server.service.script;
18 17
19 18 import com.google.common.util.concurrent.Futures;
20 19 import com.google.common.util.concurrent.ListenableFuture;
  20 +import com.google.common.util.concurrent.ListeningExecutorService;
  21 +import com.google.common.util.concurrent.MoreExecutors;
21 22 import delight.nashornsandbox.NashornSandbox;
22 23 import delight.nashornsandbox.NashornSandboxes;
23 24 import jdk.nashorn.api.scripting.NashornScriptEngineFactory;
  25 +import lombok.EqualsAndHashCode;
  26 +import lombok.Getter;
24 27 import lombok.extern.slf4j.Slf4j;
25   -import org.apache.commons.lang3.tuple.Pair;
  28 +import org.thingsboard.server.common.data.id.EntityId;
26 29
27 30 import javax.annotation.PostConstruct;
28 31 import javax.annotation.PreDestroy;
... ... @@ -42,17 +45,20 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic
42 45 private NashornSandbox sandbox;
43 46 private ScriptEngine engine;
44 47 private ExecutorService monitorExecutorService;
  48 + private ListeningExecutorService evalExecutorService;
45 49
46 50 private final Map<UUID, String> functionsMap = new ConcurrentHashMap<>();
47   - private final Map<UUID,AtomicInteger> blackListedFunctions = new ConcurrentHashMap<>();
48   - private final Map<String, Pair<UUID, AtomicInteger>> scriptToId = new ConcurrentHashMap<>();
49   - private final Map<UUID, AtomicInteger> scriptIdToCount = new ConcurrentHashMap<>();
  51 + private final Map<BlackListKey, AtomicInteger> blackListedFunctions = new ConcurrentHashMap<>();
  52 +
  53 + private final Map<String, ScriptInfo> scriptKeyToInfo = new ConcurrentHashMap<>();
  54 + private final Map<UUID, ScriptInfo> scriptIdToInfo = new ConcurrentHashMap<>();
50 55
51 56 @PostConstruct
52 57 public void init() {
53 58 if (useJsSandbox()) {
54 59 sandbox = NashornSandboxes.create();
55 60 monitorExecutorService = Executors.newFixedThreadPool(getMonitorThreadPoolSize());
  61 + evalExecutorService = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10));
56 62 sandbox.setExecutor(monitorExecutorService);
57 63 sandbox.setMaxCPUTime(getMaxCpuTime());
58 64 sandbox.allowNoBraces(false);
... ... @@ -65,9 +71,12 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic
65 71
66 72 @PreDestroy
67 73 public void stop() {
68   - if (monitorExecutorService != null) {
  74 + if (monitorExecutorService != null) {
69 75 monitorExecutorService.shutdownNow();
70 76 }
  77 + if (evalExecutorService != null) {
  78 + evalExecutorService.shutdownNow();
  79 + }
71 80 }
72 81
73 82 protected abstract boolean useJsSandbox();
... ... @@ -80,33 +89,39 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic
80 89
81 90 @Override
82 91 public ListenableFuture<UUID> eval(JsScriptType scriptType, String scriptBody, String... argNames) {
83   - Pair<UUID, AtomicInteger> deduplicated = deduplicate(scriptType, scriptBody);
84   - UUID scriptId = deduplicated.getLeft();
85   - AtomicInteger duplicateCount = deduplicated.getRight();
86   -
87   - if(duplicateCount.compareAndSet(0, 1)) {
88   - String functionName = "invokeInternal_" + scriptId.toString().replace('-', '_');
89   - String jsScript = generateJsScript(scriptType, functionName, scriptBody, argNames);
90   - try {
91   - if (useJsSandbox()) {
92   - sandbox.eval(jsScript);
93   - } else {
94   - engine.eval(jsScript);
  92 + ScriptInfo scriptInfo = deduplicate(scriptType, scriptBody);
  93 + UUID scriptId = scriptInfo.getId();
  94 + AtomicInteger duplicateCount = scriptInfo.getCount();
  95 +
  96 + synchronized (scriptInfo.getLock()) {
  97 + if (duplicateCount.compareAndSet(0, 1)) {
  98 + try {
  99 + evaluate(scriptId, scriptType, scriptBody, argNames);
  100 + } catch (Exception e) {
  101 + duplicateCount.decrementAndGet();
  102 + log.warn("Failed to compile JS script: {}", e.getMessage(), e);
  103 + return Futures.immediateFailedFuture(e);
95 104 }
96   - functionsMap.put(scriptId, functionName);
97   - } catch (Exception e) {
98   - duplicateCount.decrementAndGet();
99   - log.warn("Failed to compile JS script: {}", e.getMessage(), e);
100   - return Futures.immediateFailedFuture(e);
  105 + } else {
  106 + duplicateCount.incrementAndGet();
101 107 }
102   - } else {
103   - duplicateCount.incrementAndGet();
104 108 }
105 109 return Futures.immediateFuture(scriptId);
106 110 }
107 111
  112 + private void evaluate(UUID scriptId, JsScriptType scriptType, String scriptBody, String... argNames) throws ScriptException {
  113 + String functionName = "invokeInternal_" + scriptId.toString().replace('-', '_');
  114 + String jsScript = generateJsScript(scriptType, functionName, scriptBody, argNames);
  115 + if (useJsSandbox()) {
  116 + sandbox.eval(jsScript);
  117 + } else {
  118 + engine.eval(jsScript);
  119 + }
  120 + functionsMap.put(scriptId, functionName);
  121 + }
  122 +
108 123 @Override
109   - public ListenableFuture<Object> invokeFunction(UUID scriptId, Object... args) {
  124 + public ListenableFuture<Object> invokeFunction(UUID scriptId, EntityId entityId, Object... args) {
110 125 String functionName = functionsMap.get(scriptId);
111 126 if (functionName == null) {
112 127 return Futures.immediateFailedFuture(new RuntimeException("No compiled script found for scriptId: [" + scriptId + "]!"));
... ... @@ -117,11 +132,12 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic
117 132 if (useJsSandbox()) {
118 133 result = sandbox.getSandboxedInvocable().invokeFunction(functionName, args);
119 134 } else {
120   - result = ((Invocable)engine).invokeFunction(functionName, args);
  135 + result = ((Invocable) engine).invokeFunction(functionName, args);
121 136 }
122 137 return Futures.immediateFuture(result);
123 138 } catch (Exception e) {
124   - blackListedFunctions.computeIfAbsent(scriptId, key -> new AtomicInteger(0)).incrementAndGet();
  139 + BlackListKey blackListKey = new BlackListKey(scriptId, entityId);
  140 + blackListedFunctions.computeIfAbsent(blackListKey, key -> new AtomicInteger(0)).incrementAndGet();
125 141 return Futures.immediateFailedFuture(e);
126 142 }
127 143 } else {
... ... @@ -131,31 +147,41 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic
131 147 }
132 148
133 149 @Override
134   - public ListenableFuture<Void> release(UUID scriptId) {
135   - AtomicInteger count = scriptIdToCount.get(scriptId);
136   - if(count != null) {
137   - if(count.decrementAndGet() > 0) {
  150 + public ListenableFuture<Void> release(UUID scriptId, EntityId entityId) {
  151 + ScriptInfo scriptInfo = scriptIdToInfo.get(scriptId);
  152 + if (scriptInfo == null) {
  153 + log.warn("Script release called for not existing script id [{}]", scriptId);
  154 + return Futures.immediateFuture(null);
  155 + }
  156 +
  157 + synchronized (scriptInfo.getLock()) {
  158 + int remainingDuplicates = scriptInfo.getCount().decrementAndGet();
  159 + if (remainingDuplicates > 0) {
138 160 return Futures.immediateFuture(null);
139 161 }
140   - }
141 162
142   - String functionName = functionsMap.get(scriptId);
143   - if (functionName != null) {
144   - try {
145   - if (useJsSandbox()) {
146   - sandbox.eval(functionName + " = undefined;");
147   - } else {
148   - engine.eval(functionName + " = undefined;");
  163 + String functionName = functionsMap.get(scriptId);
  164 + if (functionName != null) {
  165 + try {
  166 + if (useJsSandbox()) {
  167 + sandbox.eval(functionName + " = undefined;");
  168 + } else {
  169 + engine.eval(functionName + " = undefined;");
  170 + }
  171 + functionsMap.remove(scriptId);
  172 + blackListedFunctions.remove(new BlackListKey(scriptId, entityId));
  173 + } catch (ScriptException e) {
  174 + log.error("Could not release script [{}] [{}]", scriptId, remainingDuplicates);
  175 + return Futures.immediateFailedFuture(e);
149 176 }
150   - functionsMap.remove(scriptId);
151   - blackListedFunctions.remove(scriptId);
152   - } catch (ScriptException e) {
153   - return Futures.immediateFailedFuture(e);
  177 + } else {
  178 + log.warn("Function name do not exist for script [{}] [{}]", scriptId, remainingDuplicates);
154 179 }
155 180 }
156 181 return Futures.immediateFuture(null);
157 182 }
158 183
  184 +
159 185 private boolean isBlackListed(UUID scriptId) {
160 186 if (blackListedFunctions.containsKey(scriptId)) {
161 187 AtomicInteger errorCount = blackListedFunctions.get(scriptId);
... ... @@ -174,15 +200,46 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic
174 200 }
175 201 }
176 202
177   - private Pair<UUID, AtomicInteger> deduplicate(JsScriptType scriptType, String scriptBody) {
178   - Pair<UUID, AtomicInteger> precomputed = Pair.of(UUID.randomUUID(), new AtomicInteger());
179   -
180   - Pair<UUID, AtomicInteger> pair = scriptToId.computeIfAbsent(deduplicateKey(scriptType, scriptBody), i -> precomputed);
181   - AtomicInteger duplicateCount = scriptIdToCount.computeIfAbsent(pair.getLeft(), i -> pair.getRight());
182   - return Pair.of(pair.getLeft(), duplicateCount);
  203 + private ScriptInfo deduplicate(JsScriptType scriptType, String scriptBody) {
  204 + ScriptInfo meta = ScriptInfo.preInit();
  205 + String key = deduplicateKey(scriptType, scriptBody);
  206 + ScriptInfo latestMeta = scriptKeyToInfo.computeIfAbsent(key, i -> meta);
  207 + return scriptIdToInfo.computeIfAbsent(latestMeta.getId(), i -> latestMeta);
183 208 }
184 209
185 210 private String deduplicateKey(JsScriptType scriptType, String scriptBody) {
186 211 return scriptType + "_" + scriptBody;
187 212 }
  213 +
  214 + @Getter
  215 + private static class ScriptInfo {
  216 + private final UUID id;
  217 + private final Object lock;
  218 + private final AtomicInteger count;
  219 +
  220 + ScriptInfo(UUID id, Object lock, AtomicInteger count) {
  221 + this.id = id;
  222 + this.lock = lock;
  223 + this.count = count;
  224 + }
  225 +
  226 + static ScriptInfo preInit() {
  227 + UUID preId = UUID.randomUUID();
  228 + AtomicInteger preCount = new AtomicInteger();
  229 + Object preLock = new Object();
  230 + return new ScriptInfo(preId, preLock, preCount);
  231 + }
  232 + }
  233 +
  234 + @EqualsAndHashCode
  235 + @Getter
  236 + private static class BlackListKey {
  237 + private final UUID scriptId;
  238 + private final EntityId entityId;
  239 +
  240 + public BlackListKey(UUID scriptId, EntityId entityId) {
  241 + this.scriptId = scriptId;
  242 + this.entityId = entityId;
  243 + }
  244 + }
188 245 }
... ...
... ... @@ -17,6 +17,7 @@
17 17 package org.thingsboard.server.service.script;
18 18
19 19 import com.google.common.util.concurrent.ListenableFuture;
  20 +import org.thingsboard.server.common.data.id.EntityId;
20 21
21 22 import java.util.UUID;
22 23
... ... @@ -24,8 +25,8 @@ public interface JsSandboxService {
24 25
25 26 ListenableFuture<UUID> eval(JsScriptType scriptType, String scriptBody, String... argNames);
26 27
27   - ListenableFuture<Object> invokeFunction(UUID scriptId, Object... args);
  28 + ListenableFuture<Object> invokeFunction(UUID scriptId, EntityId entityId, Object... args);
28 29
29   - ListenableFuture<Void> release(UUID scriptId);
  30 + ListenableFuture<Void> release(UUID scriptId, EntityId entityId);
30 31
31 32 }
... ...
... ... @@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
21 21 import com.google.common.collect.Sets;
22 22 import lombok.extern.slf4j.Slf4j;
23 23 import org.apache.commons.lang3.StringUtils;
  24 +import org.thingsboard.server.common.data.id.EntityId;
24 25 import org.thingsboard.server.common.msg.TbMsg;
25 26 import org.thingsboard.server.common.msg.TbMsgMetaData;
26 27
... ... @@ -39,9 +40,11 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S
39 40 private final JsSandboxService sandboxService;
40 41
41 42 private final UUID scriptId;
  43 + private final EntityId entityId;
42 44
43   - public RuleNodeJsScriptEngine(JsSandboxService sandboxService, String script, String... argNames) {
  45 + public RuleNodeJsScriptEngine(JsSandboxService sandboxService, EntityId entityId, String script, String... argNames) {
44 46 this.sandboxService = sandboxService;
  47 + this.entityId = entityId;
45 48 try {
46 49 this.scriptId = this.sandboxService.eval(JsScriptType.RULE_NODE_SCRIPT, script, argNames).get();
47 50 } catch (Exception e) {
... ... @@ -162,7 +165,7 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S
162 165 private JsonNode executeScript(TbMsg msg) throws ScriptException {
163 166 try {
164 167 String[] inArgs = prepareArgs(msg);
165   - String eval = sandboxService.invokeFunction(this.scriptId, inArgs[0], inArgs[1], inArgs[2]).get().toString();
  168 + String eval = sandboxService.invokeFunction(this.scriptId, this.entityId, inArgs[0], inArgs[1], inArgs[2]).get().toString();
166 169 return mapper.readTree(eval);
167 170 } catch (ExecutionException e) {
168 171 if (e.getCause() instanceof ScriptException) {
... ... @@ -176,6 +179,6 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S
176 179 }
177 180
178 181 public void destroy() {
179   - sandboxService.release(this.scriptId);
  182 + sandboxService.release(this.scriptId, this.entityId);
180 183 }
181 184 }
... ...
... ... @@ -17,7 +17,7 @@
17 17
18 18 -->
19 19 <!DOCTYPE configuration>
20   -<configuration>
  20 +<configuration scan="true" scanPeriod="10 seconds">
21 21
22 22 <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
23 23 <encoder>
... ...
... ... @@ -21,12 +21,18 @@ import org.junit.After;
21 21 import org.junit.Before;
22 22 import org.junit.Test;
23 23 import org.thingsboard.rule.engine.api.ScriptEngine;
  24 +import org.thingsboard.server.common.data.id.EntityId;
  25 +import org.thingsboard.server.common.data.id.RuleNodeId;
24 26 import org.thingsboard.server.common.msg.TbMsg;
25 27 import org.thingsboard.server.common.msg.TbMsgMetaData;
26 28
27 29 import javax.script.ScriptException;
28   -
  30 +import java.util.Map;
29 31 import java.util.Set;
  32 +import java.util.UUID;
  33 +import java.util.concurrent.*;
  34 +import java.util.concurrent.atomic.AtomicBoolean;
  35 +import java.util.concurrent.atomic.AtomicInteger;
30 36
31 37 import static org.junit.Assert.*;
32 38
... ... @@ -35,6 +41,8 @@ public class RuleNodeJsScriptEngineTest {
35 41 private ScriptEngine scriptEngine;
36 42 private TestNashornJsSandboxService jsSandboxService;
37 43
  44 + private EntityId ruleNodeId = new RuleNodeId(UUIDs.timeBased());
  45 +
38 46 @Before
39 47 public void beforeTest() throws Exception {
40 48 jsSandboxService = new TestNashornJsSandboxService(false, 1, 100, 3);
... ... @@ -48,7 +56,7 @@ public class RuleNodeJsScriptEngineTest {
48 56 @Test
49 57 public void msgCanBeUpdated() throws ScriptException {
50 58 String function = "metadata.temp = metadata.temp * 10; return {metadata: metadata};";
51   - scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, function);
  59 + scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, ruleNodeId, function);
52 60
53 61 TbMsgMetaData metaData = new TbMsgMetaData();
54 62 metaData.putValue("temp", "7");
... ... @@ -65,7 +73,7 @@ public class RuleNodeJsScriptEngineTest {
65 73 @Test
66 74 public void newAttributesCanBeAddedInMsg() throws ScriptException {
67 75 String function = "metadata.newAttr = metadata.humidity - msg.passed; return {metadata: metadata};";
68   - scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, function);
  76 + scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, ruleNodeId, function);
69 77 TbMsgMetaData metaData = new TbMsgMetaData();
70 78 metaData.putValue("temp", "7");
71 79 metaData.putValue("humidity", "99");
... ... @@ -81,7 +89,7 @@ public class RuleNodeJsScriptEngineTest {
81 89 @Test
82 90 public void payloadCanBeUpdated() throws ScriptException {
83 91 String function = "msg.passed = msg.passed * metadata.temp; msg.bigObj.newProp = 'Ukraine'; return {msg: msg};";
84   - scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, function);
  92 + scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, ruleNodeId, function);
85 93 TbMsgMetaData metaData = new TbMsgMetaData();
86 94 metaData.putValue("temp", "7");
87 95 metaData.putValue("humidity", "99");
... ... @@ -99,7 +107,7 @@ public class RuleNodeJsScriptEngineTest {
99 107 @Test
100 108 public void metadataAccessibleForFilter() throws ScriptException {
101 109 String function = "return metadata.humidity < 15;";
102   - scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, function);
  110 + scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, ruleNodeId, function);
103 111 TbMsgMetaData metaData = new TbMsgMetaData();
104 112 metaData.putValue("temp", "7");
105 113 metaData.putValue("humidity", "99");
... ... @@ -113,7 +121,7 @@ public class RuleNodeJsScriptEngineTest {
113 121 @Test
114 122 public void dataAccessibleForFilter() throws ScriptException {
115 123 String function = "return msg.passed < 15 && msg.name === 'Vit' && metadata.temp == 7 && msg.bigObj.prop == 42;";
116   - scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, function);
  124 + scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, ruleNodeId, function);
117 125 TbMsgMetaData metaData = new TbMsgMetaData();
118 126 metaData.putValue("temp", "7");
119 127 metaData.putValue("humidity", "99");
... ... @@ -134,7 +142,7 @@ public class RuleNodeJsScriptEngineTest {
134 142 "};\n" +
135 143 "\n" +
136 144 "return nextRelation(metadata, msg);";
137   - scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, jsCode);
  145 + scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, ruleNodeId, jsCode);
138 146 TbMsgMetaData metaData = new TbMsgMetaData();
139 147 metaData.putValue("temp", "10");
140 148 metaData.putValue("humidity", "99");
... ... @@ -156,7 +164,7 @@ public class RuleNodeJsScriptEngineTest {
156 164 "};\n" +
157 165 "\n" +
158 166 "return nextRelation(metadata, msg);";
159   - scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, jsCode);
  167 + scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, ruleNodeId, jsCode);
160 168 TbMsgMetaData metaData = new TbMsgMetaData();
161 169 metaData.putValue("temp", "10");
162 170 metaData.putValue("humidity", "99");
... ... @@ -168,4 +176,75 @@ public class RuleNodeJsScriptEngineTest {
168 176 scriptEngine.destroy();
169 177 }
170 178
  179 + @Test
  180 + public void concurrentReleasedCorrectly() throws InterruptedException, ExecutionException {
  181 + String code = "metadata.temp = metadata.temp * 10; return {metadata: metadata};";
  182 +
  183 + int repeat = 1000;
  184 + ExecutorService service = Executors.newFixedThreadPool(repeat);
  185 + Map<UUID, Object> scriptIds = new ConcurrentHashMap<>();
  186 + CountDownLatch startLatch = new CountDownLatch(repeat);
  187 + CountDownLatch finishLatch = new CountDownLatch(repeat);
  188 + AtomicInteger failedCount = new AtomicInteger(0);
  189 +
  190 + for (int i = 0; i < repeat; i++) {
  191 + service.submit(() -> runScript(startLatch, finishLatch, failedCount, scriptIds, code));
  192 + }
  193 +
  194 + finishLatch.await();
  195 + assertTrue(scriptIds.size() == 1);
  196 + assertTrue(failedCount.get() == 0);
  197 +
  198 + CountDownLatch nextStart = new CountDownLatch(repeat);
  199 + CountDownLatch nextFinish = new CountDownLatch(repeat);
  200 + for (int i = 0; i < repeat; i++) {
  201 + service.submit(() -> runScript(nextStart, nextFinish, failedCount, scriptIds, code));
  202 + }
  203 +
  204 + nextFinish.await();
  205 + assertTrue(scriptIds.size() == 1);
  206 + assertTrue(failedCount.get() == 0);
  207 + service.shutdownNow();
  208 + }
  209 +
  210 + @Test
  211 + public void concurrentFailedEvaluationShouldThrowException() throws InterruptedException {
  212 + String code = "metadata.temp = metadata.temp * 10; urn {metadata: metadata};";
  213 +
  214 + int repeat = 10000;
  215 + ExecutorService service = Executors.newFixedThreadPool(repeat);
  216 + Map<UUID, Object> scriptIds = new ConcurrentHashMap<>();
  217 + CountDownLatch startLatch = new CountDownLatch(repeat);
  218 + CountDownLatch finishLatch = new CountDownLatch(repeat);
  219 + AtomicInteger failedCount = new AtomicInteger(0);
  220 + for (int i = 0; i < repeat; i++) {
  221 + service.submit(() -> {
  222 + service.submit(() -> runScript(startLatch, finishLatch, failedCount, scriptIds, code));
  223 + });
  224 + }
  225 +
  226 + finishLatch.await();
  227 + assertTrue(scriptIds.isEmpty());
  228 + assertEquals(repeat, failedCount.get());
  229 + service.shutdownNow();
  230 + }
  231 +
  232 + private void runScript(CountDownLatch startLatch, CountDownLatch finishLatch, AtomicInteger failedCount,
  233 + Map<UUID, Object> scriptIds, String code) {
  234 + try {
  235 + for (int k = 0; k < 10; k++) {
  236 + startLatch.countDown();
  237 + startLatch.await();
  238 + UUID scriptId = jsSandboxService.eval(JsScriptType.RULE_NODE_SCRIPT, code).get();
  239 + scriptIds.put(scriptId, new Object());
  240 + jsSandboxService.invokeFunction(scriptId, ruleNodeId, "{}", "{}", "TEXT").get();
  241 + jsSandboxService.release(scriptId, ruleNodeId).get();
  242 + }
  243 + } catch (Throwable th) {
  244 + failedCount.incrementAndGet();
  245 + } finally {
  246 + finishLatch.countDown();
  247 + }
  248 + }
  249 +
171 250 }
\ No newline at end of file
... ...