From a4559f9114360adb8994438ec510ce7d176b9b0c Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 11 Sep 2018 14:17:29 +0300 Subject: [PATCH 01/12] JSON Forms improvements. --- ui/src/app/components/json-form.directive.js | 1 + .../components/react/json-form-rc-select.jsx | 68 ++++++++++++++++--- .../react/json-form-schema-form.jsx | 11 ++- 3 files changed, 70 insertions(+), 10 deletions(-) diff --git a/ui/src/app/components/json-form.directive.js b/ui/src/app/components/json-form.directive.js index 5749271c5d..b016f57d89 100644 --- a/ui/src/app/components/json-form.directive.js +++ b/ui/src/app/components/json-form.directive.js @@ -82,6 +82,7 @@ function JsonForm($compile, $templateCache, $mdColorPicker) { val = undefined; } selectOrSet(key, scope.model, val); + scope.formProps.model = scope.model; }, onColorClick: function(event, key, val) { scope.showColorPicker(event, val); diff --git a/ui/src/app/components/react/json-form-rc-select.jsx b/ui/src/app/components/react/json-form-rc-select.jsx index ffa0331d97..83fa929301 100644 --- a/ui/src/app/components/react/json-form-rc-select.jsx +++ b/ui/src/app/components/react/json-form-rc-select.jsx @@ -27,39 +27,90 @@ class ThingsboardRcSelect extends React.Component { this.onDeselect = this.onDeselect.bind(this); this.onBlur = this.onBlur.bind(this); this.onFocus = this.onFocus.bind(this); - let emptyValue = this.props.form.schema.type === 'array'? [] : null; this.state = { - currentValue: this.props.value || emptyValue, + currentValue: this.keyToCurrentValue(this.props.value, this.props.form.schema.type === 'array'), items: this.props.form.items, focused: false }; } + keyToCurrentValue(key, isArray) { + var currentValue = isArray ? [] : null; + if (isArray) { + var keys = key; + if (keys) { + for (var i = 0; i < keys.length; i++) { + currentValue.push({key: keys[i], label: this.labelFromKey(keys[i])}); + } + } + } else { + currentValue = {key: key, label: this.labelFromKey(key)}; + } + return currentValue; + } + + labelFromKey(key) { + let label = key || ''; + if (key) { + for (var i=0;i -1) { v.splice(index, 1); } this.setState({ currentValue: v }); - this.props.onChangeValidate(v); + this.props.onChangeValidate(this.arrayValues(v)); } } @@ -105,6 +156,7 @@ class ThingsboardRcSelect extends React.Component { combobox={this.props.form.combobox} disabled={this.props.form.readonly} value={this.state.currentValue} + labelInValue={true} onSelect={this.onSelect} onDeselect={this.onDeselect} onFocus={this.onFocus} diff --git a/ui/src/app/components/react/json-form-schema-form.jsx b/ui/src/app/components/react/json-form-schema-form.jsx index e006eb93d7..09712f26c5 100644 --- a/ui/src/app/components/react/json-form-schema-form.jsx +++ b/ui/src/app/components/react/json-form-schema-form.jsx @@ -63,11 +63,15 @@ class ThingsboardSchemaForm extends React.Component { this.onChange = this.onChange.bind(this); this.onColorClick = this.onColorClick.bind(this); + this.hasConditions = false; } onChange(key, val) { //console.log('SchemaForm.onChange', key, val); this.props.onModelChange(key, val); + if (this.hasConditions) { + this.forceUpdate(); + } } onColorClick(event, key, val) { @@ -81,8 +85,11 @@ class ThingsboardSchemaForm extends React.Component { console.log('Invalid field: \"' + form.key[0] + '\"!'); return null; } - if(form.condition && eval(form.condition) === false) { - return null; + if(form.condition) { + this.hasConditions = true; + if (eval(form.condition) === false) { + return null; + } } return } From 831f6201dbf5d4d85c6d38f9a47de05a50e5ac8e Mon Sep 17 00:00:00 2001 From: vparomskiy Date: Tue, 11 Sep 2018 14:36:24 +0300 Subject: [PATCH 02/12] sync deduplicated script evaluation/release --- .../actors/ruleChain/DefaultTbContext.java | 2 +- .../controller/RuleChainController.java | 2 +- .../AbstractNashornJsSandboxService.java | 157 ++++++++++++------ .../service/script/JsSandboxService.java | 5 +- .../script/RuleNodeJsScriptEngine.java | 9 +- application/src/main/resources/logback.xml | 2 +- .../script/RuleNodeJsScriptEngineTest.java | 95 ++++++++++- 7 files changed, 206 insertions(+), 66 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index 7279347b1c..d5260da10b 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -154,7 +154,7 @@ class DefaultTbContext implements TbContext { @Override public ScriptEngine createJsScriptEngine(String script, String... argNames) { - return new RuleNodeJsScriptEngine(mainCtx.getJsSandbox(), script, argNames); + return new RuleNodeJsScriptEngine(mainCtx.getJsSandbox(), nodeCtx.getSelf().getId(), script, argNames); } @Override diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java index 86b8fdacd7..82ab1702fa 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java +++ b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java @@ -276,7 +276,7 @@ public class RuleChainController extends BaseController { String errorText = ""; ScriptEngine engine = null; try { - engine = new RuleNodeJsScriptEngine(jsSandboxService, script, argNames); + engine = new RuleNodeJsScriptEngine(jsSandboxService, getCurrentUser().getId(), script, argNames); TbMsg inMsg = new TbMsg(UUIDs.timeBased(), msgType, null, new TbMsgMetaData(metadata), data, null, null, 0L); switch (scriptType) { case "update": diff --git a/application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsSandboxService.java b/application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsSandboxService.java index 724f652609..a0c4d5e4de 100644 --- a/application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsSandboxService.java +++ b/application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsSandboxService.java @@ -13,16 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.thingsboard.server.service.script; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; import delight.nashornsandbox.NashornSandbox; import delight.nashornsandbox.NashornSandboxes; import jdk.nashorn.api.scripting.NashornScriptEngineFactory; +import lombok.EqualsAndHashCode; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.tuple.Pair; +import org.thingsboard.server.common.data.id.EntityId; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; @@ -42,17 +45,20 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic private NashornSandbox sandbox; private ScriptEngine engine; private ExecutorService monitorExecutorService; + private ListeningExecutorService evalExecutorService; private final Map functionsMap = new ConcurrentHashMap<>(); - private final Map blackListedFunctions = new ConcurrentHashMap<>(); - private final Map> scriptToId = new ConcurrentHashMap<>(); - private final Map scriptIdToCount = new ConcurrentHashMap<>(); + private final Map blackListedFunctions = new ConcurrentHashMap<>(); + + private final Map scriptKeyToInfo = new ConcurrentHashMap<>(); + private final Map scriptIdToInfo = new ConcurrentHashMap<>(); @PostConstruct public void init() { if (useJsSandbox()) { sandbox = NashornSandboxes.create(); monitorExecutorService = Executors.newFixedThreadPool(getMonitorThreadPoolSize()); + evalExecutorService = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10)); sandbox.setExecutor(monitorExecutorService); sandbox.setMaxCPUTime(getMaxCpuTime()); sandbox.allowNoBraces(false); @@ -65,9 +71,12 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic @PreDestroy public void stop() { - if (monitorExecutorService != null) { + if (monitorExecutorService != null) { monitorExecutorService.shutdownNow(); } + if (evalExecutorService != null) { + evalExecutorService.shutdownNow(); + } } protected abstract boolean useJsSandbox(); @@ -80,33 +89,39 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic @Override public ListenableFuture eval(JsScriptType scriptType, String scriptBody, String... argNames) { - Pair deduplicated = deduplicate(scriptType, scriptBody); - UUID scriptId = deduplicated.getLeft(); - AtomicInteger duplicateCount = deduplicated.getRight(); - - if(duplicateCount.compareAndSet(0, 1)) { - String functionName = "invokeInternal_" + scriptId.toString().replace('-', '_'); - String jsScript = generateJsScript(scriptType, functionName, scriptBody, argNames); - try { - if (useJsSandbox()) { - sandbox.eval(jsScript); - } else { - engine.eval(jsScript); + ScriptInfo scriptInfo = deduplicate(scriptType, scriptBody); + UUID scriptId = scriptInfo.getId(); + AtomicInteger duplicateCount = scriptInfo.getCount(); + + synchronized (scriptInfo.getLock()) { + if (duplicateCount.compareAndSet(0, 1)) { + try { + evaluate(scriptId, scriptType, scriptBody, argNames); + } catch (Exception e) { + duplicateCount.decrementAndGet(); + log.warn("Failed to compile JS script: {}", e.getMessage(), e); + return Futures.immediateFailedFuture(e); } - functionsMap.put(scriptId, functionName); - } catch (Exception e) { - duplicateCount.decrementAndGet(); - log.warn("Failed to compile JS script: {}", e.getMessage(), e); - return Futures.immediateFailedFuture(e); + } else { + duplicateCount.incrementAndGet(); } - } else { - duplicateCount.incrementAndGet(); } return Futures.immediateFuture(scriptId); } + private void evaluate(UUID scriptId, JsScriptType scriptType, String scriptBody, String... argNames) throws ScriptException { + String functionName = "invokeInternal_" + scriptId.toString().replace('-', '_'); + String jsScript = generateJsScript(scriptType, functionName, scriptBody, argNames); + if (useJsSandbox()) { + sandbox.eval(jsScript); + } else { + engine.eval(jsScript); + } + functionsMap.put(scriptId, functionName); + } + @Override - public ListenableFuture invokeFunction(UUID scriptId, Object... args) { + public ListenableFuture invokeFunction(UUID scriptId, EntityId entityId, Object... args) { String functionName = functionsMap.get(scriptId); if (functionName == null) { return Futures.immediateFailedFuture(new RuntimeException("No compiled script found for scriptId: [" + scriptId + "]!")); @@ -117,11 +132,12 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic if (useJsSandbox()) { result = sandbox.getSandboxedInvocable().invokeFunction(functionName, args); } else { - result = ((Invocable)engine).invokeFunction(functionName, args); + result = ((Invocable) engine).invokeFunction(functionName, args); } return Futures.immediateFuture(result); } catch (Exception e) { - blackListedFunctions.computeIfAbsent(scriptId, key -> new AtomicInteger(0)).incrementAndGet(); + BlackListKey blackListKey = new BlackListKey(scriptId, entityId); + blackListedFunctions.computeIfAbsent(blackListKey, key -> new AtomicInteger(0)).incrementAndGet(); return Futures.immediateFailedFuture(e); } } else { @@ -131,31 +147,41 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic } @Override - public ListenableFuture release(UUID scriptId) { - AtomicInteger count = scriptIdToCount.get(scriptId); - if(count != null) { - if(count.decrementAndGet() > 0) { + public ListenableFuture release(UUID scriptId, EntityId entityId) { + ScriptInfo scriptInfo = scriptIdToInfo.get(scriptId); + if (scriptInfo == null) { + log.warn("Script release called for not existing script id [{}]", scriptId); + return Futures.immediateFuture(null); + } + + synchronized (scriptInfo.getLock()) { + int remainingDuplicates = scriptInfo.getCount().decrementAndGet(); + if (remainingDuplicates > 0) { return Futures.immediateFuture(null); } - } - String functionName = functionsMap.get(scriptId); - if (functionName != null) { - try { - if (useJsSandbox()) { - sandbox.eval(functionName + " = undefined;"); - } else { - engine.eval(functionName + " = undefined;"); + String functionName = functionsMap.get(scriptId); + if (functionName != null) { + try { + if (useJsSandbox()) { + sandbox.eval(functionName + " = undefined;"); + } else { + engine.eval(functionName + " = undefined;"); + } + functionsMap.remove(scriptId); + blackListedFunctions.remove(new BlackListKey(scriptId, entityId)); + } catch (ScriptException e) { + log.error("Could not release script [{}] [{}]", scriptId, remainingDuplicates); + return Futures.immediateFailedFuture(e); } - functionsMap.remove(scriptId); - blackListedFunctions.remove(scriptId); - } catch (ScriptException e) { - return Futures.immediateFailedFuture(e); + } else { + log.warn("Function name do not exist for script [{}] [{}]", scriptId, remainingDuplicates); } } return Futures.immediateFuture(null); } + private boolean isBlackListed(UUID scriptId) { if (blackListedFunctions.containsKey(scriptId)) { AtomicInteger errorCount = blackListedFunctions.get(scriptId); @@ -174,15 +200,46 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic } } - private Pair deduplicate(JsScriptType scriptType, String scriptBody) { - Pair precomputed = Pair.of(UUID.randomUUID(), new AtomicInteger()); - - Pair pair = scriptToId.computeIfAbsent(deduplicateKey(scriptType, scriptBody), i -> precomputed); - AtomicInteger duplicateCount = scriptIdToCount.computeIfAbsent(pair.getLeft(), i -> pair.getRight()); - return Pair.of(pair.getLeft(), duplicateCount); + private ScriptInfo deduplicate(JsScriptType scriptType, String scriptBody) { + ScriptInfo meta = ScriptInfo.preInit(); + String key = deduplicateKey(scriptType, scriptBody); + ScriptInfo latestMeta = scriptKeyToInfo.computeIfAbsent(key, i -> meta); + return scriptIdToInfo.computeIfAbsent(latestMeta.getId(), i -> latestMeta); } private String deduplicateKey(JsScriptType scriptType, String scriptBody) { return scriptType + "_" + scriptBody; } + + @Getter + private static class ScriptInfo { + private final UUID id; + private final Object lock; + private final AtomicInteger count; + + ScriptInfo(UUID id, Object lock, AtomicInteger count) { + this.id = id; + this.lock = lock; + this.count = count; + } + + static ScriptInfo preInit() { + UUID preId = UUID.randomUUID(); + AtomicInteger preCount = new AtomicInteger(); + Object preLock = new Object(); + return new ScriptInfo(preId, preLock, preCount); + } + } + + @EqualsAndHashCode + @Getter + private static class BlackListKey { + private final UUID scriptId; + private final EntityId entityId; + + public BlackListKey(UUID scriptId, EntityId entityId) { + this.scriptId = scriptId; + this.entityId = entityId; + } + } } diff --git a/application/src/main/java/org/thingsboard/server/service/script/JsSandboxService.java b/application/src/main/java/org/thingsboard/server/service/script/JsSandboxService.java index ee86c62a25..5e1c676443 100644 --- a/application/src/main/java/org/thingsboard/server/service/script/JsSandboxService.java +++ b/application/src/main/java/org/thingsboard/server/service/script/JsSandboxService.java @@ -17,6 +17,7 @@ package org.thingsboard.server.service.script; import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.id.EntityId; import java.util.UUID; @@ -24,8 +25,8 @@ public interface JsSandboxService { ListenableFuture eval(JsScriptType scriptType, String scriptBody, String... argNames); - ListenableFuture invokeFunction(UUID scriptId, Object... args); + ListenableFuture invokeFunction(UUID scriptId, EntityId entityId, Object... args); - ListenableFuture release(UUID scriptId); + ListenableFuture release(UUID scriptId, EntityId entityId); } diff --git a/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java b/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java index 2ba87ec8da..f2b5fd7c12 100644 --- a/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java +++ b/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.Sets; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -39,9 +40,11 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S private final JsSandboxService sandboxService; private final UUID scriptId; + private final EntityId entityId; - public RuleNodeJsScriptEngine(JsSandboxService sandboxService, String script, String... argNames) { + public RuleNodeJsScriptEngine(JsSandboxService sandboxService, EntityId entityId, String script, String... argNames) { this.sandboxService = sandboxService; + this.entityId = entityId; try { this.scriptId = this.sandboxService.eval(JsScriptType.RULE_NODE_SCRIPT, script, argNames).get(); } catch (Exception e) { @@ -162,7 +165,7 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S private JsonNode executeScript(TbMsg msg) throws ScriptException { try { String[] inArgs = prepareArgs(msg); - String eval = sandboxService.invokeFunction(this.scriptId, inArgs[0], inArgs[1], inArgs[2]).get().toString(); + String eval = sandboxService.invokeFunction(this.scriptId, this.entityId, inArgs[0], inArgs[1], inArgs[2]).get().toString(); return mapper.readTree(eval); } catch (ExecutionException e) { if (e.getCause() instanceof ScriptException) { @@ -176,6 +179,6 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S } public void destroy() { - sandboxService.release(this.scriptId); + sandboxService.release(this.scriptId, this.entityId); } } diff --git a/application/src/main/resources/logback.xml b/application/src/main/resources/logback.xml index 978a570e66..dcfc9301b2 100644 --- a/application/src/main/resources/logback.xml +++ b/application/src/main/resources/logback.xml @@ -17,7 +17,7 @@ --> - + diff --git a/application/src/test/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngineTest.java b/application/src/test/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngineTest.java index ea7038442d..88961dc256 100644 --- a/application/src/test/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngineTest.java +++ b/application/src/test/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngineTest.java @@ -21,12 +21,18 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.thingsboard.rule.engine.api.ScriptEngine; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import javax.script.ScriptException; - +import java.util.Map; import java.util.Set; +import java.util.UUID; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import static org.junit.Assert.*; @@ -35,6 +41,8 @@ public class RuleNodeJsScriptEngineTest { private ScriptEngine scriptEngine; private TestNashornJsSandboxService jsSandboxService; + private EntityId ruleNodeId = new RuleNodeId(UUIDs.timeBased()); + @Before public void beforeTest() throws Exception { jsSandboxService = new TestNashornJsSandboxService(false, 1, 100, 3); @@ -48,7 +56,7 @@ public class RuleNodeJsScriptEngineTest { @Test public void msgCanBeUpdated() throws ScriptException { String function = "metadata.temp = metadata.temp * 10; return {metadata: metadata};"; - scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, function); + scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, ruleNodeId, function); TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue("temp", "7"); @@ -65,7 +73,7 @@ public class RuleNodeJsScriptEngineTest { @Test public void newAttributesCanBeAddedInMsg() throws ScriptException { String function = "metadata.newAttr = metadata.humidity - msg.passed; return {metadata: metadata};"; - scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, function); + scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, ruleNodeId, function); TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue("temp", "7"); metaData.putValue("humidity", "99"); @@ -81,7 +89,7 @@ public class RuleNodeJsScriptEngineTest { @Test public void payloadCanBeUpdated() throws ScriptException { String function = "msg.passed = msg.passed * metadata.temp; msg.bigObj.newProp = 'Ukraine'; return {msg: msg};"; - scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, function); + scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, ruleNodeId, function); TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue("temp", "7"); metaData.putValue("humidity", "99"); @@ -99,7 +107,7 @@ public class RuleNodeJsScriptEngineTest { @Test public void metadataAccessibleForFilter() throws ScriptException { String function = "return metadata.humidity < 15;"; - scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, function); + scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, ruleNodeId, function); TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue("temp", "7"); metaData.putValue("humidity", "99"); @@ -113,7 +121,7 @@ public class RuleNodeJsScriptEngineTest { @Test public void dataAccessibleForFilter() throws ScriptException { String function = "return msg.passed < 15 && msg.name === 'Vit' && metadata.temp == 7 && msg.bigObj.prop == 42;"; - scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, function); + scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, ruleNodeId, function); TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue("temp", "7"); metaData.putValue("humidity", "99"); @@ -134,7 +142,7 @@ public class RuleNodeJsScriptEngineTest { "};\n" + "\n" + "return nextRelation(metadata, msg);"; - scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, jsCode); + scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, ruleNodeId, jsCode); TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue("temp", "10"); metaData.putValue("humidity", "99"); @@ -156,7 +164,7 @@ public class RuleNodeJsScriptEngineTest { "};\n" + "\n" + "return nextRelation(metadata, msg);"; - scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, jsCode); + scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, ruleNodeId, jsCode); TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue("temp", "10"); metaData.putValue("humidity", "99"); @@ -168,4 +176,75 @@ public class RuleNodeJsScriptEngineTest { scriptEngine.destroy(); } + @Test + public void concurrentReleasedCorrectly() throws InterruptedException, ExecutionException { + String code = "metadata.temp = metadata.temp * 10; return {metadata: metadata};"; + + int repeat = 1000; + ExecutorService service = Executors.newFixedThreadPool(repeat); + Map scriptIds = new ConcurrentHashMap<>(); + CountDownLatch startLatch = new CountDownLatch(repeat); + CountDownLatch finishLatch = new CountDownLatch(repeat); + AtomicInteger failedCount = new AtomicInteger(0); + + for (int i = 0; i < repeat; i++) { + service.submit(() -> runScript(startLatch, finishLatch, failedCount, scriptIds, code)); + } + + finishLatch.await(); + assertTrue(scriptIds.size() == 1); + assertTrue(failedCount.get() == 0); + + CountDownLatch nextStart = new CountDownLatch(repeat); + CountDownLatch nextFinish = new CountDownLatch(repeat); + for (int i = 0; i < repeat; i++) { + service.submit(() -> runScript(nextStart, nextFinish, failedCount, scriptIds, code)); + } + + nextFinish.await(); + assertTrue(scriptIds.size() == 1); + assertTrue(failedCount.get() == 0); + service.shutdownNow(); + } + + @Test + public void concurrentFailedEvaluationShouldThrowException() throws InterruptedException { + String code = "metadata.temp = metadata.temp * 10; urn {metadata: metadata};"; + + int repeat = 10000; + ExecutorService service = Executors.newFixedThreadPool(repeat); + Map scriptIds = new ConcurrentHashMap<>(); + CountDownLatch startLatch = new CountDownLatch(repeat); + CountDownLatch finishLatch = new CountDownLatch(repeat); + AtomicInteger failedCount = new AtomicInteger(0); + for (int i = 0; i < repeat; i++) { + service.submit(() -> { + service.submit(() -> runScript(startLatch, finishLatch, failedCount, scriptIds, code)); + }); + } + + finishLatch.await(); + assertTrue(scriptIds.isEmpty()); + assertEquals(repeat, failedCount.get()); + service.shutdownNow(); + } + + private void runScript(CountDownLatch startLatch, CountDownLatch finishLatch, AtomicInteger failedCount, + Map scriptIds, String code) { + try { + for (int k = 0; k < 10; k++) { + startLatch.countDown(); + startLatch.await(); + UUID scriptId = jsSandboxService.eval(JsScriptType.RULE_NODE_SCRIPT, code).get(); + scriptIds.put(scriptId, new Object()); + jsSandboxService.invokeFunction(scriptId, ruleNodeId, "{}", "{}", "TEXT").get(); + jsSandboxService.release(scriptId, ruleNodeId).get(); + } + } catch (Throwable th) { + failedCount.incrementAndGet(); + } finally { + finishLatch.countDown(); + } + } + } \ No newline at end of file From 5ea053b71b385e53dc92728ea0486e39b3851b7c Mon Sep 17 00:00:00 2001 From: vparomskiy Date: Tue, 11 Sep 2018 16:33:39 +0300 Subject: [PATCH 03/12] blacklisted Script error should show initial exception --- .../AbstractNashornJsSandboxService.java | 91 +++++++++++-------- .../script/RuleNodeJsScriptEngine.java | 4 +- 2 files changed, 55 insertions(+), 40 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsSandboxService.java b/application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsSandboxService.java index a0c4d5e4de..1d89c9d860 100644 --- a/application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsSandboxService.java +++ b/application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsSandboxService.java @@ -17,13 +17,13 @@ package org.thingsboard.server.service.script; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; -import com.google.common.util.concurrent.MoreExecutors; import delight.nashornsandbox.NashornSandbox; import delight.nashornsandbox.NashornSandboxes; import jdk.nashorn.api.scripting.NashornScriptEngineFactory; +import lombok.Data; import lombok.EqualsAndHashCode; import lombok.Getter; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.id.EntityId; @@ -45,10 +45,9 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic private NashornSandbox sandbox; private ScriptEngine engine; private ExecutorService monitorExecutorService; - private ListeningExecutorService evalExecutorService; private final Map functionsMap = new ConcurrentHashMap<>(); - private final Map blackListedFunctions = new ConcurrentHashMap<>(); + private final Map blackListedFunctions = new ConcurrentHashMap<>(); private final Map scriptKeyToInfo = new ConcurrentHashMap<>(); private final Map scriptIdToInfo = new ConcurrentHashMap<>(); @@ -58,7 +57,6 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic if (useJsSandbox()) { sandbox = NashornSandboxes.create(); monitorExecutorService = Executors.newFixedThreadPool(getMonitorThreadPoolSize()); - evalExecutorService = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10)); sandbox.setExecutor(monitorExecutorService); sandbox.setMaxCPUTime(getMaxCpuTime()); sandbox.allowNoBraces(false); @@ -74,9 +72,6 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic if (monitorExecutorService != null) { monitorExecutorService.shutdownNow(); } - if (evalExecutorService != null) { - evalExecutorService.shutdownNow(); - } } protected abstract boolean useJsSandbox(); @@ -124,26 +119,35 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic public ListenableFuture invokeFunction(UUID scriptId, EntityId entityId, Object... args) { String functionName = functionsMap.get(scriptId); if (functionName == null) { - return Futures.immediateFailedFuture(new RuntimeException("No compiled script found for scriptId: [" + scriptId + "]!")); - } - if (!isBlackListed(scriptId)) { - try { - Object result; - if (useJsSandbox()) { - result = sandbox.getSandboxedInvocable().invokeFunction(functionName, args); - } else { - result = ((Invocable) engine).invokeFunction(functionName, args); - } - return Futures.immediateFuture(result); - } catch (Exception e) { - BlackListKey blackListKey = new BlackListKey(scriptId, entityId); - blackListedFunctions.computeIfAbsent(blackListKey, key -> new AtomicInteger(0)).incrementAndGet(); - return Futures.immediateFailedFuture(e); - } + String message = "No compiled script found for scriptId: [" + scriptId + "]!"; + log.warn(message); + return Futures.immediateFailedFuture(new RuntimeException(message)); + } + + BlackListInfo blackListInfo = blackListedFunctions.get(new BlackListKey(scriptId, entityId)); + if (blackListInfo != null && blackListInfo.getCount() >= getMaxErrors()) { + RuntimeException throwable = new RuntimeException("Script is blacklisted due to maximum error count " + getMaxErrors() + "!", blackListInfo.getCause()); + throwable.printStackTrace(); + return Futures.immediateFailedFuture(throwable); + } + + try { + return invoke(functionName, args); + } catch (Exception e) { + BlackListKey blackListKey = new BlackListKey(scriptId, entityId); + blackListedFunctions.computeIfAbsent(blackListKey, key -> new BlackListInfo()).incrementWithReason(e); + return Futures.immediateFailedFuture(e); + } + } + + private ListenableFuture invoke(String functionName, Object... args) throws ScriptException, NoSuchMethodException { + Object result; + if (useJsSandbox()) { + result = sandbox.getSandboxedInvocable().invokeFunction(functionName, args); } else { - return Futures.immediateFailedFuture( - new RuntimeException("Script is blacklisted due to maximum error count " + getMaxErrors() + "!")); + result = ((Invocable) engine).invokeFunction(functionName, args); } + return Futures.immediateFuture(result); } @Override @@ -182,15 +186,6 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic } - private boolean isBlackListed(UUID scriptId) { - if (blackListedFunctions.containsKey(scriptId)) { - AtomicInteger errorCount = blackListedFunctions.get(scriptId); - return errorCount.get() >= getMaxErrors(); - } else { - return false; - } - } - private String generateJsScript(JsScriptType scriptType, String functionName, String scriptBody, String... argNames) { switch (scriptType) { case RULE_NODE_SCRIPT: @@ -233,13 +228,33 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic @EqualsAndHashCode @Getter + @RequiredArgsConstructor private static class BlackListKey { private final UUID scriptId; private final EntityId entityId; - public BlackListKey(UUID scriptId, EntityId entityId) { - this.scriptId = scriptId; - this.entityId = entityId; + } + + @Data + private static class BlackListInfo { + private final AtomicInteger count; + private Exception ex; + + BlackListInfo() { + this.count = new AtomicInteger(0); + } + + void incrementWithReason(Exception e) { + count.incrementAndGet(); + ex = e; + } + + int getCount() { + return count.get(); + } + + Exception getCause() { + return ex; } } } diff --git a/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java b/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java index f2b5fd7c12..767dc05a23 100644 --- a/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java +++ b/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java @@ -171,10 +171,10 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S if (e.getCause() instanceof ScriptException) { throw (ScriptException)e.getCause(); } else { - throw new ScriptException("Failed to execute js script: " + e.getMessage()); + throw new ScriptException(e); } } catch (Exception e) { - throw new ScriptException("Failed to execute js script: " + e.getMessage()); + throw new ScriptException(e); } } From 086537770528018ede18f02ae83c60c12385979c Mon Sep 17 00:00:00 2001 From: Mitia Shvayka Date: Tue, 11 Sep 2018 17:01:26 +0300 Subject: [PATCH 04/12] fix handle direction --- .../engine/metadata/TbGetTelemetryNode.java | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java index fe35ef6b35..5a5e9c6630 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java @@ -1,12 +1,12 @@ /** * Copyright © 2016-2018 The Thingsboard Authors - * + *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -26,10 +26,8 @@ import org.thingsboard.rule.engine.api.*; import org.thingsboard.rule.engine.api.util.DonAsynchron; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; -import org.thingsboard.server.common.data.kv.BaseTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.TsKvEntry; -import org.thingsboard.server.common.data.kv.TsKvQuery; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; @@ -39,8 +37,7 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; -import static org.thingsboard.rule.engine.metadata.TbGetTelemetryNodeConfiguration.FETCH_MODE_ALL; -import static org.thingsboard.rule.engine.metadata.TbGetTelemetryNodeConfiguration.MAX_FETCH_SIZE; +import static org.thingsboard.rule.engine.metadata.TbGetTelemetryNodeConfiguration.*; import static org.thingsboard.server.common.data.kv.Aggregation.NONE; /** @@ -64,6 +61,7 @@ public class TbGetTelemetryNode implements TbNode { private long endTsOffset; private int limit; private ObjectMapper mapper; + private String fetchMode; @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { @@ -72,6 +70,7 @@ public class TbGetTelemetryNode implements TbNode { startTsOffset = TimeUnit.valueOf(config.getStartIntervalTimeUnit()).toMillis(config.getStartInterval()); endTsOffset = TimeUnit.valueOf(config.getEndIntervalTimeUnit()).toMillis(config.getEndInterval()); limit = config.getFetchMode().equals(FETCH_MODE_ALL) ? MAX_FETCH_SIZE : 1; + fetchMode = config.getFetchMode(); mapper = new ObjectMapper(); mapper.configure(JsonGenerator.Feature.QUOTE_FIELD_NAMES, false); mapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true); @@ -96,14 +95,18 @@ public class TbGetTelemetryNode implements TbNode { } } - //TODO: handle direction; private List buildQueries() { long ts = System.currentTimeMillis(); long startTs = ts - startTsOffset; long endTs = ts - endTsOffset; - + String orderBy; + if (fetchMode.equals(FETCH_MODE_FIRST) || fetchMode.equals(FETCH_MODE_ALL)) { + orderBy = "ASC"; + } else { + orderBy = "DESC"; + } return tsKeyNames.stream() - .map(key -> new BaseReadTsKvQuery(key, startTs, endTs, 1, limit, NONE)) + .map(key -> new BaseReadTsKvQuery(key, startTs, endTs, 1, limit, NONE, orderBy)) .collect(Collectors.toList()); } @@ -116,7 +119,7 @@ public class TbGetTelemetryNode implements TbNode { } for (String key : tsKeyNames) { - if(resultNode.has(key)){ + if (resultNode.has(key)) { msg.getMetaData().putValue(key, resultNode.get(key).toString()); } } @@ -127,11 +130,11 @@ public class TbGetTelemetryNode implements TbNode { } private void processArray(ObjectNode node, TsKvEntry entry) { - if(node.has(entry.getKey())){ + if (node.has(entry.getKey())) { ArrayNode arrayNode = (ArrayNode) node.get(entry.getKey()); ObjectNode obj = buildNode(entry); arrayNode.add(obj); - }else { + } else { ArrayNode arrayNode = mapper.createArrayNode(); ObjectNode obj = buildNode(entry); arrayNode.add(obj); From 8c91b7fb765c24f9763b6b31f27697d4a2b0f5ff Mon Sep 17 00:00:00 2001 From: vparomskiy Date: Tue, 11 Sep 2018 17:12:01 +0300 Subject: [PATCH 05/12] fix license headers --- .../rule/engine/metadata/TbGetTelemetryNode.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java index 5a5e9c6630..260669f9ff 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java @@ -1,12 +1,12 @@ /** * Copyright © 2016-2018 The Thingsboard Authors - *

+ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. From dba1c026a773453191da6eefc30a9aa4a9e55bcb Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 11 Sep 2018 17:36:54 +0300 Subject: [PATCH 06/12] UI: Fix datetime period selector. --- ui/src/app/components/datetime-period.tpl.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/src/app/components/datetime-period.tpl.html b/ui/src/app/components/datetime-period.tpl.html index 605c3a8b20..7479576f96 100644 --- a/ui/src/app/components/datetime-period.tpl.html +++ b/ui/src/app/components/datetime-period.tpl.html @@ -18,14 +18,14 @@

+ > + mdp-auto-switch="true">
+ > + mdp-auto-switch="true">
\ No newline at end of file From 01e06fcd344ef23ecb56c9b57e77170cebcb2b14 Mon Sep 17 00:00:00 2001 From: Dima Landiak Date: Tue, 11 Sep 2018 18:11:04 +0300 Subject: [PATCH 07/12] jpa orderBy fix --- .../server/dao/sql/timeseries/JpaTimeseriesDao.java | 5 ++++- .../server/dao/sql/timeseries/TsKvRepository.java | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java index 1eb2f0026d..761138649d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java @@ -26,6 +26,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.UUIDConverter; import org.thingsboard.server.common.data.id.EntityId; @@ -238,7 +239,9 @@ public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService imp query.getKey(), query.getStartTs(), query.getEndTs(), - new PageRequest(0, query.getLimit())))); + new PageRequest(0, query.getLimit(), + new Sort(Sort.Direction.fromString( + query.getOrderBy()), "ts"))))); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/TsKvRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/TsKvRepository.java index 2b39d2596e..4c743e5ab6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/TsKvRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/TsKvRepository.java @@ -35,7 +35,7 @@ public interface TsKvRepository extends CrudRepository :startTs AND tskv.ts < :endTs ORDER BY tskv.ts DESC") + "AND tskv.ts > :startTs AND tskv.ts < :endTs") List findAllWithLimit(@Param("entityId") String entityId, @Param("entityType") EntityType entityType, @Param("entityKey") String key, From 8ea7058da49e42439b94802515ca098e3a6d05a4 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 12 Sep 2018 12:54:29 +0300 Subject: [PATCH 08/12] Alarms and Entities table widgets improvements. --- ui/src/app/api/subscription.js | 8 + ui/src/app/locale/locale.constant-en_US.json | 10 +- .../widget/lib/alarm-status-filter-panel.scss | 31 +++ .../lib/alarm-status-filter-panel.tpl.html | 28 +++ ui/src/app/widget/lib/alarms-table-widget.js | 190 ++++++++++++++++-- .../app/widget/lib/alarms-table-widget.scss | 30 +++ .../widget/lib/alarms-table-widget.tpl.html | 40 ++-- .../app/widget/lib/display-columns-panel.scss | 31 +++ .../widget/lib/display-columns-panel.tpl.html | 24 +++ .../app/widget/lib/entities-table-widget.js | 79 +++++++- .../app/widget/lib/entities-table-widget.scss | 30 +++ .../widget/lib/entities-table-widget.tpl.html | 27 ++- 12 files changed, 486 insertions(+), 42 deletions(-) create mode 100644 ui/src/app/widget/lib/alarm-status-filter-panel.scss create mode 100644 ui/src/app/widget/lib/alarm-status-filter-panel.tpl.html create mode 100644 ui/src/app/widget/lib/display-columns-panel.scss create mode 100644 ui/src/app/widget/lib/display-columns-panel.tpl.html diff --git a/ui/src/app/api/subscription.js b/ui/src/app/api/subscription.js index 350b1ef0b7..a66c4b1efa 100644 --- a/ui/src/app/api/subscription.js +++ b/ui/src/app/api/subscription.js @@ -267,6 +267,14 @@ export default class Subscription { } else { this.startWatchingTimewindow(); } + registration = this.ctx.$scope.$watch(function () { + return subscription.alarmSearchStatus; + }, function (newAlarmSearchStatus, prevAlarmSearchStatus) { + if (!angular.equals(newAlarmSearchStatus, prevAlarmSearchStatus)) { + subscription.update(); + } + }, true); + this.registrations.push(registration); } initDataSubscription() { diff --git a/ui/src/app/locale/locale.constant-en_US.json b/ui/src/app/locale/locale.constant-en_US.json index 6321d63f46..2caf01331b 100644 --- a/ui/src/app/locale/locale.constant-en_US.json +++ b/ui/src/app/locale/locale.constant-en_US.json @@ -133,8 +133,13 @@ "min-polling-interval-message": "At least 1 sec polling interval is allowed.", "aknowledge-alarms-title": "Acknowledge { count, plural, 1 {1 alarm} other {# alarms} }", "aknowledge-alarms-text": "Are you sure you want to acknowledge { count, plural, 1 {1 alarm} other {# alarms} }?", + "aknowledge-alarm-title": "Acknowledge Alarm", + "aknowledge-alarm-text": "Are you sure you want to acknowledge Alarm?", "clear-alarms-title": "Clear { count, plural, 1 {1 alarm} other {# alarms} }", - "clear-alarms-text": "Are you sure you want to clear { count, plural, 1 {1 alarm} other {# alarms} }?" + "clear-alarms-text": "Are you sure you want to clear { count, plural, 1 {1 alarm} other {# alarms} }?", + "clear-alarm-title": "Clear Alarm", + "clear-alarm-text": "Are you sure you want to clear Alarm?", + "alarm-status-filter": "Alarm Status Filter" }, "alias": { "add": "Add alias", @@ -748,7 +753,8 @@ "entity-name": "Entity name", "details": "Entity details", "no-entities-prompt": "No entities found", - "no-data": "No data to display" + "no-data": "No data to display", + "columns-to-display": "Columns to Display" }, "event": { "event-type": "Event type", diff --git a/ui/src/app/widget/lib/alarm-status-filter-panel.scss b/ui/src/app/widget/lib/alarm-status-filter-panel.scss new file mode 100644 index 0000000000..4bf6eeef11 --- /dev/null +++ b/ui/src/app/widget/lib/alarm-status-filter-panel.scss @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2018 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.tb-alarm-status-filter-panel { + min-width: 300px; + overflow: hidden; + background: #fff; + border-radius: 4px; + box-shadow: + 0 7px 8px -4px rgba(0, 0, 0, .2), + 0 13px 19px 2px rgba(0, 0, 0, .14), + 0 5px 24px 4px rgba(0, 0, 0, .12); + + md-content { + overflow: hidden; + background-color: #fff; + } +} diff --git a/ui/src/app/widget/lib/alarm-status-filter-panel.tpl.html b/ui/src/app/widget/lib/alarm-status-filter-panel.tpl.html new file mode 100644 index 0000000000..45e8414ef8 --- /dev/null +++ b/ui/src/app/widget/lib/alarm-status-filter-panel.tpl.html @@ -0,0 +1,28 @@ + + + + + + + {{ ('alarm.search-status.' + searchStatus) | translate }} + + + diff --git a/ui/src/app/widget/lib/alarms-table-widget.js b/ui/src/app/widget/lib/alarms-table-widget.js index 0696a7b873..6ae17e0459 100644 --- a/ui/src/app/widget/lib/alarms-table-widget.js +++ b/ui/src/app/widget/lib/alarms-table-widget.js @@ -14,11 +14,15 @@ * limitations under the License. */ import './alarms-table-widget.scss'; +import './display-columns-panel.scss'; +import './alarm-status-filter-panel.scss'; /* eslint-disable import/no-unresolved, import/default */ import alarmsTableWidgetTemplate from './alarms-table-widget.tpl.html'; import alarmDetailsDialogTemplate from '../../alarm/alarm-details-dialog.tpl.html'; +import displayColumnsPanelTemplate from './display-columns-panel.tpl.html'; +import alarmStatusFilterPanelTemplate from './alarm-status-filter-panel.tpl.html'; /* eslint-enable import/no-unresolved, import/default */ @@ -45,7 +49,7 @@ function AlarmsTableWidget() { } /*@ngInject*/ -function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDialog, $document, $translate, $q, $timeout, alarmService, utils, types) { +function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDialog, $mdPanel, $document, $translate, $q, $timeout, alarmService, utils, types) { var vm = this; vm.stylesInfo = {}; @@ -60,6 +64,7 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia vm.selectedAlarms = [] vm.alarmSource = null; + vm.alarmSearchStatus = null; vm.allAlarms = []; vm.currentAlarm = null; @@ -95,14 +100,20 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia vm.onPaginate = onPaginate; vm.onRowClick = onRowClick; vm.onActionButtonClick = onActionButtonClick; + vm.actionEnabled = actionEnabled; vm.isCurrent = isCurrent; vm.openAlarmDetails = openAlarmDetails; vm.ackAlarms = ackAlarms; + vm.ackAlarm = ackAlarm; vm.clearAlarms = clearAlarms; + vm.clearAlarm = clearAlarm; vm.cellStyle = cellStyle; vm.cellContent = cellContent; + vm.editAlarmStatusFilter = editAlarmStatusFilter; + vm.editColumnsToDisplay = editColumnsToDisplay; + $scope.$watch('vm.ctx', function() { if (vm.ctx) { vm.settings = vm.ctx.settings; @@ -158,7 +169,41 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia vm.ctx.widgetActions = [ vm.searchAction ]; - vm.actionCellDescriptors = vm.ctx.actionsApi.getActionDescriptors('actionCellButton'); + vm.displayDetails = angular.isDefined(vm.settings.displayDetails) ? vm.settings.displayDetails : true; + vm.allowAcknowledgment = angular.isDefined(vm.settings.allowAcknowledgment) ? vm.settings.allowAcknowledgment : true; + vm.allowClear = angular.isDefined(vm.settings.allowClear) ? vm.settings.allowClear : true; + + if (vm.displayDetails) { + vm.actionCellDescriptors.push( + { + displayName: $translate.instant('alarm.details'), + icon: 'more_horiz', + details: true + } + ); + } + + if (vm.allowAcknowledgment) { + vm.actionCellDescriptors.push( + { + displayName: $translate.instant('alarm.acknowledge'), + icon: 'done', + acknowledge: true + } + ); + } + + if (vm.allowClear) { + vm.actionCellDescriptors.push( + { + displayName: $translate.instant('alarm.clear'), + icon: 'clear', + clear: true + } + ); + } + + vm.actionCellDescriptors = vm.actionCellDescriptors.concat(vm.ctx.actionsApi.getActionDescriptors('actionCellButton')); if (vm.settings.alarmsTitle && vm.settings.alarmsTitle.length) { vm.alarmsTitle = utils.customTranslation(vm.settings.alarmsTitle, vm.settings.alarmsTitle); @@ -170,9 +215,6 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia vm.enableSelection = angular.isDefined(vm.settings.enableSelection) ? vm.settings.enableSelection : true; vm.searchAction.show = angular.isDefined(vm.settings.enableSearch) ? vm.settings.enableSearch : true; - vm.displayDetails = angular.isDefined(vm.settings.displayDetails) ? vm.settings.displayDetails : true; - vm.allowAcknowledgment = angular.isDefined(vm.settings.allowAcknowledgment) ? vm.settings.allowAcknowledgment : true; - vm.allowClear = angular.isDefined(vm.settings.allowClear) ? vm.settings.allowClear : true; if (!vm.allowAcknowledgment && !vm.allowClear) { vm.enableSelection = false; } @@ -305,16 +347,35 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia } function onActionButtonClick($event, alarm, actionDescriptor) { - if ($event) { - $event.stopPropagation(); + if (actionDescriptor.details) { + vm.openAlarmDetails($event, alarm); + } else if (actionDescriptor.acknowledge) { + vm.ackAlarm($event, alarm); + } else if (actionDescriptor.clear) { + vm.clearAlarm($event, alarm); + } else { + if ($event) { + $event.stopPropagation(); + } + var entityId; + var entityName; + if (alarm && alarm.originator) { + entityId = alarm.originator; + entityName = alarm.originatorName; + } + vm.ctx.actionsApi.handleWidgetAction($event, actionDescriptor, entityId, entityName, {alarm: alarm}); } - var entityId; - var entityName; - if (alarm && alarm.originator) { - entityId = alarm.originator; - entityName = alarm.originatorName; + } + + function actionEnabled(alarm, actionDescriptor) { + if (actionDescriptor.acknowledge) { + return (alarm.status == types.alarmStatus.activeUnack || + alarm.status == types.alarmStatus.clearedUnack); + } else if (actionDescriptor.clear) { + return (alarm.status == types.alarmStatus.activeAck || + alarm.status == types.alarmStatus.activeUnack); } - vm.ctx.actionsApi.handleWidgetAction($event, actionDescriptor, entityId, entityName, { alarm: alarm }); + return true; } function isCurrent(alarm) { @@ -387,6 +448,25 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia } } + function ackAlarm($event, alarm) { + if ($event) { + $event.stopPropagation(); + } + var confirm = $mdDialog.confirm() + .targetEvent($event) + .title($translate.instant('alarm.aknowledge-alarm-title')) + .htmlContent($translate.instant('alarm.aknowledge-alarm-text')) + .ariaLabel($translate.instant('alarm.acknowledge')) + .cancel($translate.instant('action.no')) + .ok($translate.instant('action.yes')); + $mdDialog.show(confirm).then(function () { + alarmService.ackAlarm(alarm.id.id).then(function () { + vm.selectedAlarms = []; + vm.subscription.update(); + }); + }); + } + function clearAlarms($event) { if ($event) { $event.stopPropagation(); @@ -420,6 +500,24 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia } } + function clearAlarm($event, alarm) { + if ($event) { + $event.stopPropagation(); + } + var confirm = $mdDialog.confirm() + .targetEvent($event) + .title($translate.instant('alarm.clear-alarm-title')) + .htmlContent($translate.instant('alarm.clear-alarm-text')) + .ariaLabel($translate.instant('alarm.clear')) + .cancel($translate.instant('action.no')) + .ok($translate.instant('action.yes')); + $mdDialog.show(confirm).then(function () { + alarmService.clearAlarm(alarm.id.id).then(function () { + vm.selectedAlarms = []; + vm.subscription.update(); + }); + }); + } function updateAlarms(preserveSelections) { if (!preserveSelections) { @@ -558,6 +656,54 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia } } + function editAlarmStatusFilter($event) { + var element = angular.element($event.target); + var position = $mdPanel.newPanelPosition() + .relativeTo(element) + .addPanelPosition($mdPanel.xPosition.ALIGN_END, $mdPanel.yPosition.BELOW); + var config = { + attachTo: angular.element($document[0].body), + controller: AlarmStatusFilterPanelController, + controllerAs: 'vm', + templateUrl: alarmStatusFilterPanelTemplate, + panelClass: 'tb-alarm-status-filter-panel', + position: position, + fullscreen: false, + locals: { + 'subscription': vm.subscription + }, + openFrom: $event, + clickOutsideToClose: true, + escapeToClose: true, + focusOnOpen: false + }; + $mdPanel.open(config); + } + + function editColumnsToDisplay($event) { + var element = angular.element($event.target); + var position = $mdPanel.newPanelPosition() + .relativeTo(element) + .addPanelPosition($mdPanel.xPosition.ALIGN_END, $mdPanel.yPosition.BELOW); + var config = { + attachTo: angular.element($document[0].body), + controller: DisplayColumnsPanelController, + controllerAs: 'vm', + templateUrl: displayColumnsPanelTemplate, + panelClass: 'tb-display-columns-panel', + position: position, + fullscreen: false, + locals: { + 'columns': vm.alarmSource.dataKeys + }, + openFrom: $event, + clickOutsideToClose: true, + escapeToClose: true, + focusOnOpen: false + }; + $mdPanel.open(config); + } + function updateAlarmSource() { vm.ctx.widgetTitle = utils.createLabelFromDatasource(vm.alarmSource, vm.alarmsTitle); @@ -570,6 +716,7 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia var dataKey = vm.alarmSource.dataKeys[d]; dataKey.title = utils.customTranslation(dataKey.label, dataKey.label); + dataKey.display = true; var keySettings = dataKey.settings; @@ -618,4 +765,19 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia } } -} \ No newline at end of file +} + +/*@ngInject*/ +function DisplayColumnsPanelController(columns) { //eslint-disable-line + + var vm = this; + vm.columns = columns; +} + +/*@ngInject*/ +function AlarmStatusFilterPanelController(subscription, types) { //eslint-disable-line + + var vm = this; + vm.types = types; + vm.subscription = subscription; +} diff --git a/ui/src/app/widget/lib/alarms-table-widget.scss b/ui/src/app/widget/lib/alarms-table-widget.scss index 2922996700..a03152360f 100644 --- a/ui/src/app/widget/lib/alarms-table-widget.scss +++ b/ui/src/app/widget/lib/alarms-table-widget.scss @@ -44,6 +44,27 @@ &.tb-data-table { table.md-table, table.md-table.md-row-select { + th.md-column { + &.tb-action-cell { + .md-button { + /* stylelint-disable-next-line selector-max-class */ + &.md-icon-button { + width: 36px; + height: 36px; + padding: 6px; + margin: 0; + /* stylelint-disable-next-line selector-max-class */ + md-icon { + width: 24px; + height: 24px; + font-size: 24px !important; + line-height: 24px !important; + } + } + } + } + } + tbody { tr { td { @@ -51,6 +72,15 @@ width: 36px; min-width: 36px; max-width: 36px; + + .md-button[disabled] { + &.md-icon-button { + /* stylelint-disable-next-line selector-max-class */ + md-icon { + color: rgba(0, 0, 0, .38); + } + } + } } } } diff --git a/ui/src/app/widget/lib/alarms-table-widget.tpl.html b/ui/src/app/widget/lib/alarms-table-widget.tpl.html index 8480058bc2..39843e19ea 100644 --- a/ui/src/app/widget/lib/alarms-table-widget.tpl.html +++ b/ui/src/app/widget/lib/alarms-table-widget.tpl.html @@ -62,33 +62,45 @@ - - - + + - - - - - - - + + - - - -
{{ key.title }}  {{ key.title }} + + filter_list + + + {{'alarm.alarm-status-filter' | translate}} + + + + view_column + + + {{'entity.columns-to-display' | translate}} + + +
- - more_horiz - - {{ 'alarm.details' | translate }} - - - {{actionDescriptor.icon}} diff --git a/ui/src/app/widget/lib/display-columns-panel.scss b/ui/src/app/widget/lib/display-columns-panel.scss new file mode 100644 index 0000000000..2e518cba96 --- /dev/null +++ b/ui/src/app/widget/lib/display-columns-panel.scss @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2018 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.tb-display-columns-panel { + min-width: 300px; + overflow: hidden; + background: #fff; + border-radius: 4px; + box-shadow: + 0 7px 8px -4px rgba(0, 0, 0, .2), + 0 13px 19px 2px rgba(0, 0, 0, .14), + 0 5px 24px 4px rgba(0, 0, 0, .12); + + md-content { + overflow: hidden; + background-color: #fff; + } +} diff --git a/ui/src/app/widget/lib/display-columns-panel.tpl.html b/ui/src/app/widget/lib/display-columns-panel.tpl.html new file mode 100644 index 0000000000..42e2471603 --- /dev/null +++ b/ui/src/app/widget/lib/display-columns-panel.tpl.html @@ -0,0 +1,24 @@ + + + + + {{ column.title }} + + diff --git a/ui/src/app/widget/lib/entities-table-widget.js b/ui/src/app/widget/lib/entities-table-widget.js index d0b629d290..620f313749 100644 --- a/ui/src/app/widget/lib/entities-table-widget.js +++ b/ui/src/app/widget/lib/entities-table-widget.js @@ -14,11 +14,13 @@ * limitations under the License. */ import './entities-table-widget.scss'; +import './display-columns-panel.scss'; /* eslint-disable import/no-unresolved, import/default */ import entitiesTableWidgetTemplate from './entities-table-widget.tpl.html'; //import entityDetailsDialogTemplate from './entitiy-details-dialog.tpl.html'; +import displayColumnsPanelTemplate from './display-columns-panel.tpl.html'; /* eslint-enable import/no-unresolved, import/default */ @@ -45,7 +47,7 @@ function EntitiesTableWidget() { } /*@ngInject*/ -function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $translate, $timeout, utils, types) { +function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $mdPanel, $document, $translate, $timeout, utils, types) { var vm = this; vm.stylesInfo = {}; @@ -98,6 +100,8 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra vm.cellStyle = cellStyle; vm.cellContent = cellContent; + vm.editColumnsToDisplay = editColumnsToDisplay; + $scope.$watch('vm.ctx', function() { if (vm.ctx && vm.ctx.defaultSubscription) { vm.settings = vm.ctx.settings; @@ -414,12 +418,37 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra } } + function editColumnsToDisplay($event) { + var element = angular.element($event.target); + var position = $mdPanel.newPanelPosition() + .relativeTo(element) + .addPanelPosition($mdPanel.xPosition.ALIGN_END, $mdPanel.yPosition.BELOW); + var config = { + attachTo: angular.element($document[0].body), + controller: DisplayColumnsPanelController, + controllerAs: 'vm', + templateUrl: displayColumnsPanelTemplate, + panelClass: 'tb-display-columns-panel', + position: position, + fullscreen: false, + locals: { + 'columns': vm.columns + }, + openFrom: $event, + clickOutsideToClose: true, + escapeToClose: true, + focusOnOpen: false + }; + $mdPanel.open(config); + } + function updateDatasources() { vm.stylesInfo = {}; vm.contentsInfo = {}; vm.columnWidth = {}; vm.dataKeys = []; + vm.columns = []; vm.allEntities = []; var datasource; @@ -429,6 +458,42 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra vm.ctx.widgetTitle = utils.createLabelFromDatasource(datasource, vm.entitiesTitle); + if (vm.displayEntityName) { + vm.columns.push( + { + name: 'entityName', + label: 'entityName', + title: vm.entityNameColumnTitle, + display: true + } + ); + vm.contentsInfo['entityName'] = { + useCellContentFunction: false + }; + vm.stylesInfo['entityName'] = { + useCellStyleFunction: false + }; + vm.columnWidth['entityName'] = '0px'; + } + + if (vm.displayEntityType) { + vm.columns.push( + { + name: 'entityType', + label: 'entityType', + title: $translate.instant('entity.entity-type'), + display: true + } + ); + vm.contentsInfo['entityType'] = { + useCellContentFunction: false + }; + vm.stylesInfo['entityType'] = { + useCellStyleFunction: false + }; + vm.columnWidth['entityType'] = '0px'; + } + for (var d = 0; d < datasource.dataKeys.length; d++ ) { dataKey = angular.copy(datasource.dataKeys[d]); if (dataKey.type == types.dataKeyType.function) { @@ -482,6 +547,9 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra var columnWidth = angular.isDefined(keySettings.columnWidth) ? keySettings.columnWidth : '0px'; vm.columnWidth[dataKey.label] = columnWidth; + + dataKey.display = true; + vm.columns.push(dataKey); } for (var i=0;i
{{vm.entityNameColumnTitle}}entity.entity-type{{ key.title }} {{ column.title }} + + view_column + + + {{'entity.columns-to-display' | translate}} + + +
{{entity.entityName}}{{entity.entityType}} + From 55ece585b5ee34967048053138b93004d0bd81c4 Mon Sep 17 00:00:00 2001 From: Chun-Yeow Yeoh Date: Fri, 14 Sep 2018 11:17:11 +0800 Subject: [PATCH 09/12] add missing API limits on CoAP POST method Signed-off-by: Chun-Yeow Yeoh --- .../server/transport/coap/CoapTransportResource.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java index bac68ffc58..8a596d7119 100644 --- a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java +++ b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java @@ -113,6 +113,12 @@ public class CoapTransportResource extends CoapResource { @Override public void handlePOST(CoapExchange exchange) { + if(quotaService.isQuotaExceeded(exchange.getSourceAddress().getHostAddress())) { + log.warn("COAP Quota exceeded for [{}:{}] . Disconnect", exchange.getSourceAddress().getHostAddress(), exchange.getSourcePort()); + exchange.respond(ResponseCode.BAD_REQUEST); + return; + } + Optional featureType = getFeatureType(exchange.advanced().getRequest()); if (!featureType.isPresent()) { log.trace("Missing feature type parameter"); From 3727519e511597dc7ec2d7db828976f1bae20eda Mon Sep 17 00:00:00 2001 From: Sergey Tarnavskiy Date: Wed, 19 Sep 2018 12:27:38 +0300 Subject: [PATCH 10/12] fixed issue with gauge value --- ui/src/app/widget/lib/CanvasDigitalGauge.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/app/widget/lib/CanvasDigitalGauge.js b/ui/src/app/widget/lib/CanvasDigitalGauge.js index ee8b0ed559..331ce6c5aa 100644 --- a/ui/src/app/widget/lib/CanvasDigitalGauge.js +++ b/ui/src/app/widget/lib/CanvasDigitalGauge.js @@ -209,7 +209,7 @@ export default class CanvasDigitalGauge extends canvasGauges.BaseGauge { this.elementValueClone.renderedValue = this._value; } if (angular.isUndefined(this.elementValueClone.renderedValue)) { - this.elementValueClone.renderedValue = options.minValue; + this.elementValueClone.renderedValue = this.value; } let context = this.contextValueClone; // clear the cache From 6c618a7db35ecc1a757f09869fcb16465f7de465 Mon Sep 17 00:00:00 2001 From: Andrew Shvayka Date: Sun, 23 Sep 2018 16:22:04 +0300 Subject: [PATCH 11/12] Fixed issue with Zookeeper reconnect --- .../cluster/discovery/ZkDiscoveryService.java | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ZkDiscoveryService.java b/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ZkDiscoveryService.java index c3ffbabbea..6f51ceed26 100644 --- a/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ZkDiscoveryService.java +++ b/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ZkDiscoveryService.java @@ -24,6 +24,8 @@ import org.apache.curator.framework.recipes.cache.ChildData; import org.apache.curator.framework.recipes.cache.PathChildrenCache; import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent; import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener; +import org.apache.curator.framework.state.ConnectionState; +import org.apache.curator.framework.state.ConnectionStateListener; import org.apache.curator.retry.RetryForever; import org.apache.curator.utils.CloseableUtils; import org.apache.zookeeper.CreateMode; @@ -127,12 +129,38 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi .creatingParentsIfNeeded() .withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(zkNodesDir + "/", SerializationUtils.serialize(self.getServerAddress())); log.info("[{}:{}] Created ZK node for current instance: {}", self.getHost(), self.getPort(), nodePath); + client.getConnectionStateListenable().addListener(checkReconnect(self)); } catch (Exception e) { log.error("Failed to create ZK node", e); throw new RuntimeException(e); } } + private ConnectionStateListener checkReconnect(ServerInstance self) { + return (client, newState) -> { + log.info("[{}:{}] ZK state changed: {}", self.getHost(), self.getPort(), newState); + if (newState == ConnectionState.LOST) { + reconnect(); + } + }; + } + + private boolean reconnectInProgress = false; + + private synchronized void reconnect() { + if (!reconnectInProgress) { + reconnectInProgress = true; + try { + client.blockUntilConnected(); + publishCurrentServer(); + } catch (InterruptedException e) { + log.error("Failed to reconnect to ZK: {}", e.getMessage(), e); + } finally { + reconnectInProgress = false; + } + } + } + @Override public void unpublishCurrentServer() { try { @@ -156,7 +184,7 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi .filter(cd -> !cd.getPath().equals(nodePath)) .map(cd -> { try { - return new ServerInstance( (ServerAddress) SerializationUtils.deserialize(cd.getData())); + return new ServerInstance((ServerAddress) SerializationUtils.deserialize(cd.getData())); } catch (NoSuchElementException e) { log.error("Failed to decode ZK node", e); throw new RuntimeException(e); @@ -198,7 +226,7 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi } ServerInstance instance; try { - ServerAddress serverAddress = SerializationUtils.deserialize(data.getData()); + ServerAddress serverAddress = SerializationUtils.deserialize(data.getData()); instance = new ServerInstance(serverAddress); } catch (SerializationException e) { log.error("Failed to decode server instance for node {}", data.getPath(), e); From 601efbc9317a0f65df0413bc800ad25870e3ebc3 Mon Sep 17 00:00:00 2001 From: Andrew Shvayka Date: Wed, 26 Sep 2018 14:57:43 +0300 Subject: [PATCH 12/12] Fixed action type for alarm audit log --- .../java/org/thingsboard/server/controller/AlarmController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java index 586e8c35ef..00885e34b0 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java @@ -83,7 +83,7 @@ public class AlarmController extends BaseController { Alarm savedAlarm = checkNotNull(alarmService.createOrUpdateAlarm(alarm)); logEntityAction(savedAlarm.getId(), savedAlarm, getCurrentUser().getCustomerId(), - savedAlarm.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null); + alarm.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null); return savedAlarm; } catch (Exception e) { logEntityAction(emptyId(EntityType.ALARM), alarm,