Commit 831f6201dbf5d4d85c6d38f9a47de05a50e5ac8e

Authored by vparomskiy
1 parent 4ba34513

sync deduplicated script evaluation/release

@@ -154,7 +154,7 @@ class DefaultTbContext implements TbContext { @@ -154,7 +154,7 @@ class DefaultTbContext implements TbContext {
154 154
155 @Override 155 @Override
156 public ScriptEngine createJsScriptEngine(String script, String... argNames) { 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 @Override 160 @Override
@@ -276,7 +276,7 @@ public class RuleChainController extends BaseController { @@ -276,7 +276,7 @@ public class RuleChainController extends BaseController {
276 String errorText = ""; 276 String errorText = "";
277 ScriptEngine engine = null; 277 ScriptEngine engine = null;
278 try { 278 try {
279 - engine = new RuleNodeJsScriptEngine(jsSandboxService, script, argNames); 279 + engine = new RuleNodeJsScriptEngine(jsSandboxService, getCurrentUser().getId(), script, argNames);
280 TbMsg inMsg = new TbMsg(UUIDs.timeBased(), msgType, null, new TbMsgMetaData(metadata), data, null, null, 0L); 280 TbMsg inMsg = new TbMsg(UUIDs.timeBased(), msgType, null, new TbMsgMetaData(metadata), data, null, null, 0L);
281 switch (scriptType) { 281 switch (scriptType) {
282 case "update": 282 case "update":
@@ -13,16 +13,19 @@ @@ -13,16 +13,19 @@
13 * See the License for the specific language governing permissions and 13 * See the License for the specific language governing permissions and
14 * limitations under the License. 14 * limitations under the License.
15 */ 15 */
16 -  
17 package org.thingsboard.server.service.script; 16 package org.thingsboard.server.service.script;
18 17
19 import com.google.common.util.concurrent.Futures; 18 import com.google.common.util.concurrent.Futures;
20 import com.google.common.util.concurrent.ListenableFuture; 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 import delight.nashornsandbox.NashornSandbox; 22 import delight.nashornsandbox.NashornSandbox;
22 import delight.nashornsandbox.NashornSandboxes; 23 import delight.nashornsandbox.NashornSandboxes;
23 import jdk.nashorn.api.scripting.NashornScriptEngineFactory; 24 import jdk.nashorn.api.scripting.NashornScriptEngineFactory;
  25 +import lombok.EqualsAndHashCode;
  26 +import lombok.Getter;
24 import lombok.extern.slf4j.Slf4j; 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 import javax.annotation.PostConstruct; 30 import javax.annotation.PostConstruct;
28 import javax.annotation.PreDestroy; 31 import javax.annotation.PreDestroy;
@@ -42,17 +45,20 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic @@ -42,17 +45,20 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic
42 private NashornSandbox sandbox; 45 private NashornSandbox sandbox;
43 private ScriptEngine engine; 46 private ScriptEngine engine;
44 private ExecutorService monitorExecutorService; 47 private ExecutorService monitorExecutorService;
  48 + private ListeningExecutorService evalExecutorService;
45 49
46 private final Map<UUID, String> functionsMap = new ConcurrentHashMap<>(); 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 @PostConstruct 56 @PostConstruct
52 public void init() { 57 public void init() {
53 if (useJsSandbox()) { 58 if (useJsSandbox()) {
54 sandbox = NashornSandboxes.create(); 59 sandbox = NashornSandboxes.create();
55 monitorExecutorService = Executors.newFixedThreadPool(getMonitorThreadPoolSize()); 60 monitorExecutorService = Executors.newFixedThreadPool(getMonitorThreadPoolSize());
  61 + evalExecutorService = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10));
56 sandbox.setExecutor(monitorExecutorService); 62 sandbox.setExecutor(monitorExecutorService);
57 sandbox.setMaxCPUTime(getMaxCpuTime()); 63 sandbox.setMaxCPUTime(getMaxCpuTime());
58 sandbox.allowNoBraces(false); 64 sandbox.allowNoBraces(false);
@@ -65,9 +71,12 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic @@ -65,9 +71,12 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic
65 71
66 @PreDestroy 72 @PreDestroy
67 public void stop() { 73 public void stop() {
68 - if (monitorExecutorService != null) { 74 + if (monitorExecutorService != null) {
69 monitorExecutorService.shutdownNow(); 75 monitorExecutorService.shutdownNow();
70 } 76 }
  77 + if (evalExecutorService != null) {
  78 + evalExecutorService.shutdownNow();
  79 + }
71 } 80 }
72 81
73 protected abstract boolean useJsSandbox(); 82 protected abstract boolean useJsSandbox();
@@ -80,33 +89,39 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic @@ -80,33 +89,39 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic
80 89
81 @Override 90 @Override
82 public ListenableFuture<UUID> eval(JsScriptType scriptType, String scriptBody, String... argNames) { 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 return Futures.immediateFuture(scriptId); 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 @Override 123 @Override
109 - public ListenableFuture<Object> invokeFunction(UUID scriptId, Object... args) { 124 + public ListenableFuture<Object> invokeFunction(UUID scriptId, EntityId entityId, Object... args) {
110 String functionName = functionsMap.get(scriptId); 125 String functionName = functionsMap.get(scriptId);
111 if (functionName == null) { 126 if (functionName == null) {
112 return Futures.immediateFailedFuture(new RuntimeException("No compiled script found for scriptId: [" + scriptId + "]!")); 127 return Futures.immediateFailedFuture(new RuntimeException("No compiled script found for scriptId: [" + scriptId + "]!"));
@@ -117,11 +132,12 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic @@ -117,11 +132,12 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic
117 if (useJsSandbox()) { 132 if (useJsSandbox()) {
118 result = sandbox.getSandboxedInvocable().invokeFunction(functionName, args); 133 result = sandbox.getSandboxedInvocable().invokeFunction(functionName, args);
119 } else { 134 } else {
120 - result = ((Invocable)engine).invokeFunction(functionName, args); 135 + result = ((Invocable) engine).invokeFunction(functionName, args);
121 } 136 }
122 return Futures.immediateFuture(result); 137 return Futures.immediateFuture(result);
123 } catch (Exception e) { 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 return Futures.immediateFailedFuture(e); 141 return Futures.immediateFailedFuture(e);
126 } 142 }
127 } else { 143 } else {
@@ -131,31 +147,41 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic @@ -131,31 +147,41 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic
131 } 147 }
132 148
133 @Override 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 return Futures.immediateFuture(null); 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 return Futures.immediateFuture(null); 181 return Futures.immediateFuture(null);
157 } 182 }
158 183
  184 +
159 private boolean isBlackListed(UUID scriptId) { 185 private boolean isBlackListed(UUID scriptId) {
160 if (blackListedFunctions.containsKey(scriptId)) { 186 if (blackListedFunctions.containsKey(scriptId)) {
161 AtomicInteger errorCount = blackListedFunctions.get(scriptId); 187 AtomicInteger errorCount = blackListedFunctions.get(scriptId);
@@ -174,15 +200,46 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic @@ -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 private String deduplicateKey(JsScriptType scriptType, String scriptBody) { 210 private String deduplicateKey(JsScriptType scriptType, String scriptBody) {
186 return scriptType + "_" + scriptBody; 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,6 +17,7 @@
17 package org.thingsboard.server.service.script; 17 package org.thingsboard.server.service.script;
18 18
19 import com.google.common.util.concurrent.ListenableFuture; 19 import com.google.common.util.concurrent.ListenableFuture;
  20 +import org.thingsboard.server.common.data.id.EntityId;
20 21
21 import java.util.UUID; 22 import java.util.UUID;
22 23
@@ -24,8 +25,8 @@ public interface JsSandboxService { @@ -24,8 +25,8 @@ public interface JsSandboxService {
24 25
25 ListenableFuture<UUID> eval(JsScriptType scriptType, String scriptBody, String... argNames); 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,6 +21,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
21 import com.google.common.collect.Sets; 21 import com.google.common.collect.Sets;
22 import lombok.extern.slf4j.Slf4j; 22 import lombok.extern.slf4j.Slf4j;
23 import org.apache.commons.lang3.StringUtils; 23 import org.apache.commons.lang3.StringUtils;
  24 +import org.thingsboard.server.common.data.id.EntityId;
24 import org.thingsboard.server.common.msg.TbMsg; 25 import org.thingsboard.server.common.msg.TbMsg;
25 import org.thingsboard.server.common.msg.TbMsgMetaData; 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,9 +40,11 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S
39 private final JsSandboxService sandboxService; 40 private final JsSandboxService sandboxService;
40 41
41 private final UUID scriptId; 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 this.sandboxService = sandboxService; 46 this.sandboxService = sandboxService;
  47 + this.entityId = entityId;
45 try { 48 try {
46 this.scriptId = this.sandboxService.eval(JsScriptType.RULE_NODE_SCRIPT, script, argNames).get(); 49 this.scriptId = this.sandboxService.eval(JsScriptType.RULE_NODE_SCRIPT, script, argNames).get();
47 } catch (Exception e) { 50 } catch (Exception e) {
@@ -162,7 +165,7 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S @@ -162,7 +165,7 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S
162 private JsonNode executeScript(TbMsg msg) throws ScriptException { 165 private JsonNode executeScript(TbMsg msg) throws ScriptException {
163 try { 166 try {
164 String[] inArgs = prepareArgs(msg); 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 return mapper.readTree(eval); 169 return mapper.readTree(eval);
167 } catch (ExecutionException e) { 170 } catch (ExecutionException e) {
168 if (e.getCause() instanceof ScriptException) { 171 if (e.getCause() instanceof ScriptException) {
@@ -176,6 +179,6 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S @@ -176,6 +179,6 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S
176 } 179 }
177 180
178 public void destroy() { 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,7 +17,7 @@
17 17
18 --> 18 -->
19 <!DOCTYPE configuration> 19 <!DOCTYPE configuration>
20 -<configuration> 20 +<configuration scan="true" scanPeriod="10 seconds">
21 21
22 <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> 22 <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
23 <encoder> 23 <encoder>
@@ -21,12 +21,18 @@ import org.junit.After; @@ -21,12 +21,18 @@ import org.junit.After;
21 import org.junit.Before; 21 import org.junit.Before;
22 import org.junit.Test; 22 import org.junit.Test;
23 import org.thingsboard.rule.engine.api.ScriptEngine; 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 import org.thingsboard.server.common.msg.TbMsg; 26 import org.thingsboard.server.common.msg.TbMsg;
25 import org.thingsboard.server.common.msg.TbMsgMetaData; 27 import org.thingsboard.server.common.msg.TbMsgMetaData;
26 28
27 import javax.script.ScriptException; 29 import javax.script.ScriptException;
28 - 30 +import java.util.Map;
29 import java.util.Set; 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 import static org.junit.Assert.*; 37 import static org.junit.Assert.*;
32 38
@@ -35,6 +41,8 @@ public class RuleNodeJsScriptEngineTest { @@ -35,6 +41,8 @@ public class RuleNodeJsScriptEngineTest {
35 private ScriptEngine scriptEngine; 41 private ScriptEngine scriptEngine;
36 private TestNashornJsSandboxService jsSandboxService; 42 private TestNashornJsSandboxService jsSandboxService;
37 43
  44 + private EntityId ruleNodeId = new RuleNodeId(UUIDs.timeBased());
  45 +
38 @Before 46 @Before
39 public void beforeTest() throws Exception { 47 public void beforeTest() throws Exception {
40 jsSandboxService = new TestNashornJsSandboxService(false, 1, 100, 3); 48 jsSandboxService = new TestNashornJsSandboxService(false, 1, 100, 3);
@@ -48,7 +56,7 @@ public class RuleNodeJsScriptEngineTest { @@ -48,7 +56,7 @@ public class RuleNodeJsScriptEngineTest {
48 @Test 56 @Test
49 public void msgCanBeUpdated() throws ScriptException { 57 public void msgCanBeUpdated() throws ScriptException {
50 String function = "metadata.temp = metadata.temp * 10; return {metadata: metadata};"; 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 TbMsgMetaData metaData = new TbMsgMetaData(); 61 TbMsgMetaData metaData = new TbMsgMetaData();
54 metaData.putValue("temp", "7"); 62 metaData.putValue("temp", "7");
@@ -65,7 +73,7 @@ public class RuleNodeJsScriptEngineTest { @@ -65,7 +73,7 @@ public class RuleNodeJsScriptEngineTest {
65 @Test 73 @Test
66 public void newAttributesCanBeAddedInMsg() throws ScriptException { 74 public void newAttributesCanBeAddedInMsg() throws ScriptException {
67 String function = "metadata.newAttr = metadata.humidity - msg.passed; return {metadata: metadata};"; 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 TbMsgMetaData metaData = new TbMsgMetaData(); 77 TbMsgMetaData metaData = new TbMsgMetaData();
70 metaData.putValue("temp", "7"); 78 metaData.putValue("temp", "7");
71 metaData.putValue("humidity", "99"); 79 metaData.putValue("humidity", "99");
@@ -81,7 +89,7 @@ public class RuleNodeJsScriptEngineTest { @@ -81,7 +89,7 @@ public class RuleNodeJsScriptEngineTest {
81 @Test 89 @Test
82 public void payloadCanBeUpdated() throws ScriptException { 90 public void payloadCanBeUpdated() throws ScriptException {
83 String function = "msg.passed = msg.passed * metadata.temp; msg.bigObj.newProp = 'Ukraine'; return {msg: msg};"; 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 TbMsgMetaData metaData = new TbMsgMetaData(); 93 TbMsgMetaData metaData = new TbMsgMetaData();
86 metaData.putValue("temp", "7"); 94 metaData.putValue("temp", "7");
87 metaData.putValue("humidity", "99"); 95 metaData.putValue("humidity", "99");
@@ -99,7 +107,7 @@ public class RuleNodeJsScriptEngineTest { @@ -99,7 +107,7 @@ public class RuleNodeJsScriptEngineTest {
99 @Test 107 @Test
100 public void metadataAccessibleForFilter() throws ScriptException { 108 public void metadataAccessibleForFilter() throws ScriptException {
101 String function = "return metadata.humidity < 15;"; 109 String function = "return metadata.humidity < 15;";
102 - scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, function); 110 + scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, ruleNodeId, function);
103 TbMsgMetaData metaData = new TbMsgMetaData(); 111 TbMsgMetaData metaData = new TbMsgMetaData();
104 metaData.putValue("temp", "7"); 112 metaData.putValue("temp", "7");
105 metaData.putValue("humidity", "99"); 113 metaData.putValue("humidity", "99");
@@ -113,7 +121,7 @@ public class RuleNodeJsScriptEngineTest { @@ -113,7 +121,7 @@ public class RuleNodeJsScriptEngineTest {
113 @Test 121 @Test
114 public void dataAccessibleForFilter() throws ScriptException { 122 public void dataAccessibleForFilter() throws ScriptException {
115 String function = "return msg.passed < 15 && msg.name === 'Vit' && metadata.temp == 7 && msg.bigObj.prop == 42;"; 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 TbMsgMetaData metaData = new TbMsgMetaData(); 125 TbMsgMetaData metaData = new TbMsgMetaData();
118 metaData.putValue("temp", "7"); 126 metaData.putValue("temp", "7");
119 metaData.putValue("humidity", "99"); 127 metaData.putValue("humidity", "99");
@@ -134,7 +142,7 @@ public class RuleNodeJsScriptEngineTest { @@ -134,7 +142,7 @@ public class RuleNodeJsScriptEngineTest {
134 "};\n" + 142 "};\n" +
135 "\n" + 143 "\n" +
136 "return nextRelation(metadata, msg);"; 144 "return nextRelation(metadata, msg);";
137 - scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, jsCode); 145 + scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, ruleNodeId, jsCode);
138 TbMsgMetaData metaData = new TbMsgMetaData(); 146 TbMsgMetaData metaData = new TbMsgMetaData();
139 metaData.putValue("temp", "10"); 147 metaData.putValue("temp", "10");
140 metaData.putValue("humidity", "99"); 148 metaData.putValue("humidity", "99");
@@ -156,7 +164,7 @@ public class RuleNodeJsScriptEngineTest { @@ -156,7 +164,7 @@ public class RuleNodeJsScriptEngineTest {
156 "};\n" + 164 "};\n" +
157 "\n" + 165 "\n" +
158 "return nextRelation(metadata, msg);"; 166 "return nextRelation(metadata, msg);";
159 - scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, jsCode); 167 + scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, ruleNodeId, jsCode);
160 TbMsgMetaData metaData = new TbMsgMetaData(); 168 TbMsgMetaData metaData = new TbMsgMetaData();
161 metaData.putValue("temp", "10"); 169 metaData.putValue("temp", "10");
162 metaData.putValue("humidity", "99"); 170 metaData.putValue("humidity", "99");
@@ -168,4 +176,75 @@ public class RuleNodeJsScriptEngineTest { @@ -168,4 +176,75 @@ public class RuleNodeJsScriptEngineTest {
168 scriptEngine.destroy(); 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 }