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,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": |
application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsSandboxService.java
@@ -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 | } |