Commit 831f6201dbf5d4d85c6d38f9a47de05a50e5ac8e
1 parent
4ba34513
sync deduplicated script evaluation/release
Showing
7 changed files
with
206 additions
and
66 deletions
... | ... | @@ -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": | ... | ... |
application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsSandboxService.java
... | ... | @@ -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 | } | ... | ... |
... | ... | @@ -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 | ... | ... |