handleRegistration(TransportProtos.LwM2MRegistrationRequestMsg msg) {
+ TenantId tenantId = new TenantId(UUID.fromString(msg.getTenantId()));
+ String deviceName = msg.getEndpoint();
+ Lock deviceCreationLock = deviceCreationLocks.computeIfAbsent(deviceName, id -> new ReentrantLock());
+ deviceCreationLock.lock();
+ try {
+ Device device = deviceService.findDeviceByTenantIdAndName(tenantId, deviceName);
+ if (device == null) {
+ device = new Device();
+ device.setTenantId(tenantId);
+ device.setName(deviceName);
+ device.setType("LwM2M");
+ device = deviceService.saveDevice(device);
+ deviceStateService.onDeviceAdded(device);
+ }
+ TransportProtos.LwM2MRegistrationResponseMsg registrationResponseMsg =
+ TransportProtos.LwM2MRegistrationResponseMsg.newBuilder()
+ .setDeviceInfo(getDeviceInfoProto(device)).build();
+ TransportProtos.LwM2MResponseMsg responseMsg = TransportProtos.LwM2MResponseMsg.newBuilder().setRegistrationMsg(registrationResponseMsg).build();
+ return Futures.immediateFuture(TransportApiResponseMsg.newBuilder().setLwM2MResponseMsg(responseMsg).build());
+ } catch (JsonProcessingException e) {
+ log.warn("[{}][{}] Failed to lookup device by gateway id and name", tenantId, deviceName, e);
+ throw new RuntimeException(e);
+ } finally {
+ deviceCreationLock.unlock();
+ }
+ }
}
diff --git a/application/src/main/java/org/thingsboard/server/service/transport/msg/TransportToDeviceActorMsgWrapper.java b/application/src/main/java/org/thingsboard/server/service/transport/msg/TransportToDeviceActorMsgWrapper.java
index d542bdbe61..74d81c0307 100644
--- a/application/src/main/java/org/thingsboard/server/service/transport/msg/TransportToDeviceActorMsgWrapper.java
+++ b/application/src/main/java/org/thingsboard/server/service/transport/msg/TransportToDeviceActorMsgWrapper.java
@@ -34,6 +34,8 @@ import java.util.UUID;
@Data
public class TransportToDeviceActorMsgWrapper implements TbActorMsg, DeviceAwareMsg, TenantAwareMsg, Serializable {
+ private static final long serialVersionUID = 7191333353202935941L;
+
private final TenantId tenantId;
private final DeviceId deviceId;
private final TransportToDeviceActorMsg msg;
diff --git a/application/src/main/java/org/thingsboard/server/utils/EventDeduplicationExecutor.java b/application/src/main/java/org/thingsboard/server/utils/EventDeduplicationExecutor.java
new file mode 100644
index 0000000000..4ce0d2f3d5
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/utils/EventDeduplicationExecutor.java
@@ -0,0 +1,85 @@
+/**
+ * Copyright © 2016-2021 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.
+ */
+package org.thingsboard.server.utils;
+
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.function.Consumer;
+
+/**
+ * This class deduplicate executions of the specified function.
+ * Useful in cluster mode, when you get event about partition change multiple times.
+ * Assuming that the function execution is expensive, we should execute it immediately when first time event occurs and
+ * later, once the processing of first event is done, process last pending task.
+ *
+ * @param parameters of the function
+ */
+@Slf4j
+public class EventDeduplicationExecutor
{
+ private final String name;
+ private final ExecutorService executor;
+ private final Consumer
function;
+ private P pendingTask;
+ private boolean busy;
+
+ public EventDeduplicationExecutor(String name, ExecutorService executor, Consumer
function) {
+ this.name = name;
+ this.executor = executor;
+ this.function = function;
+ }
+
+ public void submit(P params) {
+ log.info("[{}] Going to submit: {}", name, params);
+ synchronized (EventDeduplicationExecutor.this) {
+ if (!busy) {
+ busy = true;
+ pendingTask = null;
+ try {
+ log.info("[{}] Submitting task: {}", name, params);
+ executor.submit(() -> {
+ try {
+ log.info("[{}] Executing task: {}", name, params);
+ function.accept(params);
+ } catch (Throwable e) {
+ log.warn("[{}] Failed to process task with parameters: {}", name, params, e);
+ throw e;
+ } finally {
+ unlockAndProcessIfAny();
+ }
+ });
+ } catch (Throwable e) {
+ log.warn("[{}] Failed to submit task with parameters: {}", name, params, e);
+ unlockAndProcessIfAny();
+ throw e;
+ }
+ } else {
+ log.info("[{}] Task is already in progress. {} pending task: {}", name, pendingTask == null ? "adding" : "updating", params);
+ pendingTask = params;
+ }
+ }
+ }
+
+ private void unlockAndProcessIfAny() {
+ synchronized (EventDeduplicationExecutor.this) {
+ busy = false;
+ if (pendingTask != null) {
+ submit(pendingTask);
+ }
+ }
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/utils/MiscUtils.java b/application/src/main/java/org/thingsboard/server/utils/MiscUtils.java
index 6ca82aa062..8ee5a5ebc7 100644
--- a/application/src/main/java/org/thingsboard/server/utils/MiscUtils.java
+++ b/application/src/main/java/org/thingsboard/server/utils/MiscUtils.java
@@ -33,6 +33,7 @@ public class MiscUtils {
return "The " + propertyName + " property need to be set!";
}
+ @SuppressWarnings("deprecation")
public static HashFunction forName(String name) {
switch (name) {
case "murmur3_32":
diff --git a/application/src/main/resources/banner.txt b/application/src/main/resources/banner.txt
index 791f878366..111465dd8a 100644
--- a/application/src/main/resources/banner.txt
+++ b/application/src/main/resources/banner.txt
@@ -1,3 +1,10 @@
+ ______ __ _ __ __
+ /_ __/ / /_ (_) ____ ____ _ _____ / /_ ____ ____ _ _____ ____/ /
+ / / / __ \ / / / __ \ / __ `/ / ___/ / __ \ / __ \ / __ `/ / ___/ / __ /
+ / / / / / / / / / / / / / /_/ / (__ ) / /_/ // /_/ // /_/ / / / / /_/ /
+/_/ /_/ /_/ /_/ /_/ /_/ \__, / /____/ /_.___/ \____/ \__,_/ /_/ \__,_/
+ /____/
+
===================================================
:: ${application.title} :: ${application.formatted-version}
===================================================
diff --git a/application/src/main/resources/logback.xml b/application/src/main/resources/logback.xml
index 6d10a74854..f8085ec985 100644
--- a/application/src/main/resources/logback.xml
+++ b/application/src/main/resources/logback.xml
@@ -30,10 +30,13 @@
-
+
+
+
+
@@ -41,4 +44,4 @@
-
\ No newline at end of file
+
diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml
index 437a672c54..3f7ef449ed 100644
--- a/application/src/main/resources/thingsboard.yml
+++ b/application/src/main/resources/thingsboard.yml
@@ -34,6 +34,7 @@ server:
log_controller_error_stack_trace: "${HTTP_LOG_CONTROLLER_ERROR_STACK_TRACE:false}"
ws:
send_timeout: "${TB_SERVER_WS_SEND_TIMEOUT:5000}"
+ ping_timeout: "${TB_SERVER_WS_PING_TIMEOUT:30000}"
limits:
# Limit the amount of sessions and subscriptions available on each server. Put values to zero to disable particular limitation
max_sessions_per_tenant: "${TB_SERVER_WS_TENANT_RATE_LIMITS_MAX_SESSIONS_PER_TENANT:0}"
@@ -118,6 +119,15 @@ security:
githubMapper:
emailUrl: "${SECURITY_OAUTH2_GITHUB_MAPPER_EMAIL_URL_KEY:https://api.github.com/user/emails}"
+# Usage statistics parameters
+usage:
+ stats:
+ report:
+ enabled: "${USAGE_STATS_REPORT_ENABLED:true}"
+ interval: "${USAGE_STATS_REPORT_INTERVAL:10}"
+ check:
+ cycle: "${USAGE_STATS_CHECK_CYCLE:60000}"
+
# Dashboard parameters
dashboard:
# Maximum allowed datapoints fetched by widgets
@@ -313,6 +323,9 @@ actors:
cache:
# caffeine or redis
type: "${CACHE_TYPE:caffeine}"
+ attributes:
+ # make sure that if cache.type is 'redis' and cache.attributes.enabled is 'true' that you change 'maxmemory-policy' Redis config property to 'allkeys-lru', 'allkeys-lfu' or 'allkeys-random'
+ enabled: "${CACHE_ATTRIBUTES_ENABLED:true}"
caffeine:
specs:
@@ -346,6 +359,9 @@ caffeine:
deviceProfiles:
timeToLiveInMinutes: 1440
maxSize: 0
+ attributes:
+ timeToLiveInMinutes: 1440
+ maxSize: 100000
redis:
# standalone or cluster
@@ -489,7 +505,7 @@ js:
# Built-in JVM JavaScript environment properties
local:
# Use Sandboxed (secured) JVM JavaScript environment
- use_js_sandbox: "${USE_LOCAL_JS_SANDBOX:true}"
+ use_js_sandbox: "${USE_LOCAL_JS_SANDBOX:false}"
# Specify thread pool size for JavaScript sandbox resource monitor
monitor_thread_pool_size: "${LOCAL_JS_SANDBOX_MONITOR_THREAD_POOL_SIZE:4}"
# Maximum CPU time in milliseconds allowed for script execution
@@ -530,6 +546,7 @@ transport:
http:
enabled: "${HTTP_ENABLED:true}"
request_timeout: "${HTTP_REQUEST_TIMEOUT:60000}"
+ max_request_timeout: "${HTTP_MAX_REQUEST_TIMEOUT:300000}"
# Local MQTT transport parameters
mqtt:
# Enable/disable mqtt transport protocol.
@@ -566,6 +583,68 @@ transport:
bind_address: "${COAP_BIND_ADDRESS:0.0.0.0}"
bind_port: "${COAP_BIND_PORT:5683}"
timeout: "${COAP_TIMEOUT:10000}"
+ # Local LwM2M transport parameters
+ lwm2m:
+ # Enable/disable lvm2m transport protocol.
+ enabled: "${LWM2M_ENABLED:true}"
+ # We choose a default timeout a bit higher to the MAX_TRANSMIT_WAIT(62-93s) which is the time from starting to
+ # send a Confirmable message to the time when an acknowledgement is no longer expected.
+ # DEFAULT_TIMEOUT = 2 * 60 * 1000l; 2 min in ms
+ timeout: "${LWM2M_TIMEOUT:120000}"
+# model_path_file: "${LWM2M_MODEL_PATH_FILE:./common/transport/lwm2m/src/main/resources/models/}"
+ model_path_file: "${LWM2M_MODEL_PATH_FILE:}"
+ recommended_ciphers: "${LWM2M_RECOMMENDED_CIPHERS:false}"
+ recommended_supported_groups: "${LWM2M_RECOMMENDED_SUPPORTED_GROUPS:true}"
+ request_pool_size: "${LWM2M_REQUEST_POOL_SIZE:100}"
+ request_error_pool_size: "${LWM2M_REQUEST_ERROR_POOL_SIZE:10}"
+ registered_pool_size: "${LWM2M_REGISTERED_POOL_SIZE:10}"
+ update_registered_pool_size: "${LWM2M_UPDATE_REGISTERED_POOL_SIZE:10}"
+ un_registered_pool_size: "${LWM2M_UN_REGISTERED_POOL_SIZE:10}"
+ secure:
+ # Certificate_x509:
+ # To get helps about files format and how to generate it, see: https://github.com/eclipse/leshan/wiki/Credential-files-format
+ # Create new X509 Certificates: common/transport/lwm2m/src/main/resources/credentials/shell/lwM2M_credentials.sh
+ key_store_type: "${LWM2M_KEYSTORE_TYPE:JKS}"
+ # key_store_type: "${LWM2M_KEYSTORE_TYPE:PKCS12}"
+# key_store_path_file: "${KEY_STORE_PATH_FILE:/usr/share/thingsboard/conf/credentials/serverKeyStore.jks}"
+ key_store_path_file: "${KEY_STORE_PATH_FILE:}"
+ key_store_password: "${LWM2M_KEYSTORE_PASSWORD_SERVER:server_ks_password}"
+ root_alias: "${LWM2M_SERVER_ROOT_CA:rootca}"
+ enable_gen_new_key_psk_rpk: "${ENABLE_GEN_NEW_KEY_PSK_RPK:false}"
+ server:
+ id: "${LWM2M_SERVER_ID:123}"
+ bind_address: "${LWM2M_BIND_ADDRESS:0.0.0.0}"
+ bind_port_no_sec: "${LWM2M_BIND_PORT_NO_SEC:5685}"
+ secure:
+ bind_address_security: "${LWM2M_BIND_ADDRESS_SECURITY:0.0.0.0}"
+ bind_port_security: "${LWM2M_BIND_PORT_SECURITY:5686}"
+ # create_rpk: "${CREATE_RPK:}"
+ # Only for RPK: Public & Private Key. If the keystore file is missing or not working
+ # - Public Key (Hex): [3059301306072a8648ce3d020106082a8648ce3d0301070342000405064b9e6762dd8d8b8a52355d7b4d8b9a3d64e6d2ee277d76c248861353f3585eeb1838e4f9e37b31fa347aef5ce3431eb54e0a2506910c5e0298817445721b]
+ # - Private Key (Hex): [308193020100301306072a8648ce3d020106082a8648ce3d030107047930770201010420dc774b309e547ceb48fee547e104ce201a9c48c449dc5414cd04e7f5cf05f67ba00a06082a8648ce3d030107a1440342000405064b9e6762dd8d8b8a52355d7b4d8b9a3d64e6d2ee277d76c248861353f3585eeb1838e4f9e37b31fa347aef5ce3431eb54e0a2506910c5e0298817445721b],
+ # - Elliptic Curve parameters : [secp256r1 [NIST P-256, X9.62 prime256v1] (1.2.840.10045.3.1.7)]
+ public_x: "${LWM2M_SERVER_PUBLIC_X:05064b9e6762dd8d8b8a52355d7b4d8b9a3d64e6d2ee277d76c248861353f358}"
+ public_y: "${LWM2M_SERVER_PUBLIC_Y:5eeb1838e4f9e37b31fa347aef5ce3431eb54e0a2506910c5e0298817445721b}"
+ private_encoded: "${LWM2M_SERVER_PRIVATE_ENCODED:308193020100301306072a8648ce3d020106082a8648ce3d030107047930770201010420dc774b309e547ceb48fee547e104ce201a9c48c449dc5414cd04e7f5cf05f67ba00a06082a8648ce3d030107a1440342000405064b9e6762dd8d8b8a52355d7b4d8b9a3d64e6d2ee277d76c248861353f3585eeb1838e4f9e37b31fa347aef5ce3431eb54e0a2506910c5e0298817445721b}" # Only Certificate_x509:
+ alias: "${LWM2M_KEYSTORE_ALIAS_SERVER:server}"
+ bootstrap:
+ enable: "${LWM2M_BOOTSTRAP_ENABLED:true}"
+ id: "${LWM2M_SERVER_ID:111}"
+ bind_address: "${LWM2M_BIND_ADDRESS_BS:0.0.0.0}"
+ bind_port_no_sec: "${LWM2M_BIND_PORT_NO_SEC_BS:5687}"
+ secure:
+ bind_address_security: "${LWM2M_BIND_ADDRESS_BS:0.0.0.0}"
+ bind_port_security: "${LWM2M_BIND_PORT_SEC_BS:5688}"
+ # Only for RPK: Public & Private Key. If the keystore file is missing or not working
+ # - Elliptic Curve parameters : [secp256r1 [NIST P-256, X9.62 prime256v1] (1.2.840.10045.3.1.7)]
+ # - Public Key (Hex): [3059301306072a8648ce3d020106082a8648ce3d030107034200045017c87a1c1768264656b3b355434b0def6edb8b9bf166a4762d9930cd730f913fc4e61bcd8901ec27c424114c3e887ed372497f0c2cf85839b8443e76988b34]
+ # - Private Key (Hex): [308193020100301306072a8648ce3d020106082a8648ce3d0301070479307702010104205ecafd90caa7be45c42e1f3f32571632b8409e6e6249d7124f4ba56fab3c8083a00a06082a8648ce3d030107a144034200045017c87a1c1768264656b3b355434b0def6edb8b9bf166a4762d9930cd730f913fc4e61bcd8901ec27c424114c3e887ed372497f0c2cf85839b8443e76988b34],
+ public_x: "${LWM2M_SERVER_PUBLIC_X_BS:5017c87a1c1768264656b3b355434b0def6edb8b9bf166a4762d9930cd730f91}"
+ public_y: "${LWM2M_SERVER_PUBLIC_Y_BS:3fc4e61bcd8901ec27c424114c3e887ed372497f0c2cf85839b8443e76988b34}"
+ private_encoded: "${LWM2M_SERVER_PRIVATE_ENCODED_BS:308193020100301306072a8648ce3d020106082a8648ce3d0301070479307702010104205ecafd90caa7be45c42e1f3f32571632b8409e6e6249d7124f4ba56fab3c8083a00a06082a8648ce3d030107a144034200045017c87a1c1768264656b3b355434b0def6edb8b9bf166a4762d9930cd730f913fc4e61bcd8901ec27c424114c3e887ed372497f0c2cf85839b8443e76988b34}" # Only Certificate_x509:
+ alias: "${LWM2M_KEYSTORE_ALIAS_BOOTSTRAP:bootstrap}"
+ # Use redis for Security and Registration stores
+ redis.enabled: "${LWM2M_REDIS_ENABLED:false}"
snmp:
enabled: "${SNMP_ENABLED:true}"
@@ -615,6 +694,10 @@ queue:
transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}"
notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}"
js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600;partitions:100}"
+ consumer-stats:
+ enabled: "${TB_QUEUE_KAFKA_CONSUMER_STATS_ENABLED:true}"
+ print-interval-ms: "${TB_QUEUE_KAFKA_CONSUMER_STATS_MIN_PRINT_INTERVAL_MS:60000}"
+ kafka-response-timeout-ms: "${TB_QUEUE_KAFKA_CONSUMER_STATS_RESPONSE_TIMEOUT_MS:1000}"
aws_sqs:
use_default_credential_provider_chain: "${TB_QUEUE_AWS_SQS_USE_DEFAULT_CREDENTIAL_PROVIDER_CHAIN:false}"
access_key_id: "${TB_QUEUE_AWS_SQS_ACCESS_KEY_ID:YOUR_KEY}"
diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
index 12f4e5fced..e25592e35e 100644
--- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
@@ -375,6 +375,10 @@ public abstract class AbstractWebTest {
return readResponse(doGetAsync(urlTemplate, urlVariables).andExpect(status().isOk()), responseClass);
}
+ protected T doGetAsyncTyped(String urlTemplate, TypeReference responseType, Object... urlVariables) throws Exception {
+ return readResponse(doGetAsync(urlTemplate, urlVariables).andExpect(status().isOk()), responseType);
+ }
+
protected ResultActions doGetAsync(String urlTemplate, Object... urlVariables) throws Exception {
MockHttpServletRequestBuilder getRequest;
getRequest = get(urlTemplate, urlVariables);
diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java
index d4e4b4e0d6..5d234d2691 100644
--- a/application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java
@@ -44,6 +44,7 @@ import org.thingsboard.server.common.data.query.EntityDataSortOrder;
import org.thingsboard.server.common.data.query.EntityKey;
import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.common.data.query.EntityListFilter;
+import org.thingsboard.server.common.data.query.EntityTypeFilter;
import org.thingsboard.server.common.data.query.FilterPredicateValue;
import org.thingsboard.server.common.data.query.KeyFilter;
import org.thingsboard.server.common.data.query.NumericFilterPredicate;
@@ -132,6 +133,14 @@ public abstract class BaseEntityQueryControllerTest extends AbstractControllerTe
count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class);
Assert.assertEquals(97, count.longValue());
+
+ EntityTypeFilter filter2 = new EntityTypeFilter();
+ filter2.setEntityType(EntityType.DEVICE);
+
+ EntityCountQuery countQuery2 = new EntityCountQuery(filter2);
+
+ Long count2 = doPostWithResponse("/api/entitiesQuery/count", countQuery2, Long.class);
+ Assert.assertEquals(97, count2.longValue());
}
@Test
@@ -198,11 +207,31 @@ public abstract class BaseEntityQueryControllerTest extends AbstractControllerTe
Assert.assertEquals(11, data.getTotalElements());
Assert.assertEquals("Device19", data.getData().get(0).getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue());
+
+ EntityTypeFilter filter2 = new EntityTypeFilter();
+ filter2.setEntityType(EntityType.DEVICE);
+
+ EntityDataSortOrder sortOrder2 = new EntityDataSortOrder(
+ new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.ASC
+ );
+ EntityDataPageLink pageLink2 = new EntityDataPageLink(10, 0, null, sortOrder2);
+ List entityFields2 = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"));
+
+ EntityDataQuery query2 = new EntityDataQuery(filter2, pageLink2, entityFields2, null, null);
+
+ PageData data2 =
+ doPostWithTypedResponse("/api/entitiesQuery/find", query2, new TypeReference>() {
+ });
+
+ Assert.assertEquals(97, data2.getTotalElements());
+ Assert.assertEquals(10, data2.getTotalPages());
+ Assert.assertTrue(data2.hasNext());
+ Assert.assertEquals(10, data2.getData().size());
+
}
@Test
public void testFindEntityDataByQueryWithAttributes() throws Exception {
-
List devices = new ArrayList<>();
List temperatures = new ArrayList<>();
List highTemperatures = new ArrayList<>();
diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java
index 35ff3703c0..c6d0073d83 100644
--- a/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java
@@ -347,8 +347,8 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes
Thread.sleep(1000);
- List