Browse Source

Merge pull request #8304 from YevhenBondarenko/feature/entity-usage

[3.5]added entity usage
pull/8323/head
Andrew Shvayka 3 years ago
committed by GitHub
parent
commit
2ad4ea765a
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 45
      application/src/main/java/org/thingsboard/server/controller/UsageInfoController.java
  2. 4
      application/src/main/resources/thingsboard.yml
  3. 101
      application/src/test/java/org/thingsboard/server/controller/BaseHomePageApiTest.java
  4. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java
  5. 26
      common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityCountService.java
  6. 4
      common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityDaoService.java
  7. 25
      common/dao-api/src/main/java/org/thingsboard/server/dao/usage/UsageInfoService.java
  8. 1
      common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java
  9. 43
      common/data/src/main/java/org/thingsboard/server/common/data/UsageInfo.java
  10. 13
      dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java
  11. 17
      dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java
  12. 15
      dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java
  13. 10
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
  14. 49
      dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityCountService.java
  15. 28
      dao/src/main/java/org/thingsboard/server/dao/entity/EntityCountCacheEvictEvent.java
  16. 41
      dao/src/main/java/org/thingsboard/server/dao/entity/EntityCountCacheKey.java
  17. 33
      dao/src/main/java/org/thingsboard/server/dao/entity/count/EntityCountCaffeineCache.java
  18. 35
      dao/src/main/java/org/thingsboard/server/dao/entity/count/EntityCountRedisCache.java
  19. 98
      dao/src/main/java/org/thingsboard/server/dao/usage/BasicUsageInfoService.java
  20. 10
      dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
  21. 3
      dao/src/test/resources/application-test.properties
  22. 9
      rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java

45
application/src/main/java/org/thingsboard/server/controller/UsageInfoController.java

@ -0,0 +1,45 @@
/**
* Copyright © 2016-2023 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.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.UsageInfo;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.dao.usage.UsageInfoService;
import org.thingsboard.server.queue.util.TbCoreComponent;
@RestController
@TbCoreComponent
@RequestMapping("/api")
@Slf4j
public class UsageInfoController extends BaseController {
@Autowired
private UsageInfoService usageInfoService;
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/usage", method = RequestMethod.GET)
@ResponseBody
public UsageInfo getTenantUsageInfo() throws ThingsboardException {
return checkNotNull(usageInfoService.getUsageInfo(getCurrentUser().getTenantId()));
}
}

4
application/src/main/resources/thingsboard.yml

@ -477,7 +477,9 @@ cache:
dashboardTitles:
timeToLiveInMinutes: "${CACHE_SPECS_DASHBOARD_TITLES_TTL:1440}"
maxSize: "${CACHE_SPECS_DASHBOARD_TITLES_MAX_SIZE:100000}"
entityCount:
timeToLiveInMinutes: "${CACHE_SPECS_ENTITY_COUNT_TTL:1440}"
maxSize: "${CACHE_SPECS_ENTITY_COUNT_MAX_SIZE:100000}"
#Disable this because it is not required.
spring.data.redis.repositories.enabled: false

101
application/src/test/java/org/thingsboard/server/controller/BaseHomePageApiTest.java

@ -26,11 +26,13 @@ import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.FeaturesInfo;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.UsageInfo;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.TenantId;
@ -49,7 +51,9 @@ import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityTypeFilter;
import org.thingsboard.server.common.data.query.TsValue;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.stats.TbApiUsageStateClient;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountUpdate;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate;
@ -69,6 +73,9 @@ public abstract class BaseHomePageApiTest extends AbstractControllerTest {
@Autowired
private TbApiUsageStateClient apiUsageStateClient;
@Autowired
private TbTenantProfileCache tenantProfileCache;
//For system administrator
@Test
public void testTenantsCountWsCmd() throws Exception {
@ -330,6 +337,100 @@ public abstract class BaseHomePageApiTest extends AbstractControllerTest {
Assert.assertTrue(featuresInfo.isOauthEnabled());
}
@Test
public void testUsageInfo() throws Exception {
loginTenantAdmin();
TenantProfile tenantProfile = tenantProfileCache.get(tenantId);
Assert.assertNotNull(tenantProfile);
DefaultTenantProfileConfiguration configuration = (DefaultTenantProfileConfiguration) tenantProfile.getProfileData().getConfiguration();
UsageInfo usageInfo = doGet("/api/usage", UsageInfo.class);
Assert.assertNotNull(usageInfo);
Assert.assertEquals(0, usageInfo.getDevices());
Assert.assertEquals(configuration.getMaxDevices(), usageInfo.getMaxDevices());
Assert.assertEquals(0, usageInfo.getAssets());
Assert.assertEquals(configuration.getMaxAssets(), usageInfo.getMaxAssets());
Assert.assertEquals(1, usageInfo.getCustomers());
Assert.assertEquals(configuration.getMaxCustomers(), usageInfo.getMaxCustomers());
Assert.assertEquals(2, usageInfo.getUsers());
Assert.assertEquals(configuration.getMaxUsers(), usageInfo.getMaxUsers());
Assert.assertEquals(0, usageInfo.getDashboards());
Assert.assertEquals(configuration.getMaxDashboards(), usageInfo.getMaxDashboards());
Assert.assertEquals(0, usageInfo.getTransportMessages());
Assert.assertEquals(configuration.getMaxTransportMessages(), usageInfo.getMaxTransportMessages());
Assert.assertEquals(0, usageInfo.getJsExecutions());
Assert.assertEquals(configuration.getMaxJSExecutions(), usageInfo.getMaxJsExecutions());
Assert.assertEquals(0, usageInfo.getEmails());
Assert.assertEquals(configuration.getMaxEmails(), usageInfo.getMaxEmails());
Assert.assertEquals(0, usageInfo.getSms());
Assert.assertEquals(configuration.getMaxSms(), usageInfo.getMaxSms());
Assert.assertEquals(0, usageInfo.getAlarms());
Assert.assertEquals(configuration.getMaxCreatedAlarms(), usageInfo.getMaxAlarms());
List<Device> devices = new ArrayList<>();
for (int i = 0; i < 97; i++) {
Device device = new Device();
device.setName("device" + i);
devices.add(doPost("/api/device", device, Device.class));
}
usageInfo = doGet("/api/usage", UsageInfo.class);
Assert.assertEquals(devices.size(), usageInfo.getDevices());
List<Asset> assets = new ArrayList<>();
for (int i = 0; i < 97; i++) {
Asset asset = new Asset();
asset.setName("asset" + i);
assets.add(doPost("/api/asset", asset, Asset.class));
}
usageInfo = doGet("/api/usage", UsageInfo.class);
Assert.assertEquals(assets.size(), usageInfo.getAssets());
List<Customer> customers = new ArrayList<>();
for (int i = 0; i < 97; i++) {
Customer customer = new Customer();
customer.setTitle("customer" + i);
customers.add(doPost("/api/customer", customer, Customer.class));
}
usageInfo = doGet("/api/usage", UsageInfo.class);
Assert.assertEquals(customers.size() + 1, usageInfo.getCustomers());
List<User> users = new ArrayList<>();
for (int i = 0; i < 97; i++) {
User user = new User();
user.setAuthority(Authority.TENANT_ADMIN);
user.setEmail(i + "user@thingsboard.org");
users.add(doPost("/api/user", user, User.class));
}
usageInfo = doGet("/api/usage", UsageInfo.class);
Assert.assertEquals(users.size() + 2, usageInfo.getUsers());
List<Dashboard> dashboards = new ArrayList<>();
for (int i = 0; i < 97; i++) {
Dashboard dashboard = new Dashboard();
dashboard.setTitle("dashboard" + i);
dashboards.add(doPost("/api/dashboard", dashboard, Dashboard.class));
}
usageInfo = doGet("/api/usage", UsageInfo.class);
Assert.assertEquals(dashboards.size(), usageInfo.getDashboards());
}
private OAuth2Info createDefaultOAuth2Info() {
return new OAuth2Info(true, Lists.newArrayList(
OAuth2ParamsInfo.builder()

3
common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java

@ -17,11 +17,11 @@ package org.thingsboard.server.dao.device;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceIdInfo;
import org.thingsboard.server.common.data.DeviceInfo;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.DeviceIdInfo;
import org.thingsboard.server.common.data.device.DeviceSearchQuery;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
@ -118,5 +118,4 @@ public interface DeviceService extends EntityDaoService {
PageData<Device> findDevicesByTenantIdAndEdgeIdAndType(TenantId tenantId, EdgeId edgeId, String type, PageLink pageLink);
long countByTenantId(TenantId tenantId);
}

26
common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityCountService.java

@ -0,0 +1,26 @@
/**
* Copyright © 2016-2023 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.dao.entity;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.TenantId;
public interface EntityCountService {
long countByTenantIdAndEntityType(TenantId tenantId, EntityType entityType);
void publishCountEntityEvictEvent(TenantId tenantId, EntityType entityType);
}

4
common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityDaoService.java

@ -26,6 +26,10 @@ public interface EntityDaoService {
Optional<HasId<?>> findEntity(TenantId tenantId, EntityId entityId);
default long countByTenantId(TenantId tenantId) {
throw new IllegalArgumentException("Not implemented for " + getEntityType());
}
EntityType getEntityType();
}

25
common/dao-api/src/main/java/org/thingsboard/server/dao/usage/UsageInfoService.java

@ -0,0 +1,25 @@
/**
* Copyright © 2016-2023 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.dao.usage;
import org.thingsboard.server.common.data.UsageInfo;
import org.thingsboard.server.common.data.id.TenantId;
public interface UsageInfoService {
UsageInfo getUsageInfo(TenantId tenantId);
}

1
common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java

@ -43,4 +43,5 @@ public class CacheConstants {
public static final String VERSION_CONTROL_TASK_CACHE = "versionControlTask";
public static final String USER_SETTINGS_CACHE = "userSettings";
public static final String DASHBOARD_TITLES_CACHE = "dashboardTitles";
public static final String ENTITY_COUNT_CACHE = "entityCount";
}

43
common/data/src/main/java/org/thingsboard/server/common/data/UsageInfo.java

@ -0,0 +1,43 @@
/**
* Copyright © 2016-2023 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.common.data;
import lombok.Data;
@Data
public class UsageInfo {
private long devices;
private long maxDevices;
private long assets;
private long maxAssets;
private long customers;
private long maxCustomers;
private long users;
private long maxUsers;
private long dashboards;
private long maxDashboards;
private long transportMessages;
private long maxTransportMessages;
private long jsExecutions;
private long maxJsExecutions;
private long emails;
private long maxEmails;
private long sms;
private long maxSms;
private long alarms;
private long maxAlarms;
}

13
dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java

@ -46,6 +46,7 @@ import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
import org.thingsboard.server.dao.entity.EntityCountService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover;
@ -83,6 +84,9 @@ public class BaseAssetService extends AbstractCachedEntityService<AssetCacheKey,
@Autowired
private DataValidator<Asset> assetValidator;
@Autowired
private EntityCountService countService;
@TransactionalEventListener(classes = AssetCacheEvictEvent.class)
@Override
public void handleEvictEvent(AssetCacheEvictEvent event) {
@ -151,6 +155,9 @@ public class BaseAssetService extends AbstractCachedEntityService<AssetCacheKey,
asset.setType(assetProfile.getName());
savedAsset = assetDao.saveAndFlush(asset.getTenantId(), asset);
publishEvictEvent(evictEvent);
if (asset.getId() == null) {
countService.publishCountEntityEvictEvent(savedAsset.getTenantId(), EntityType.ASSET);
}
} catch (Exception t) {
handleEvictEvent(evictEvent);
checkConstraintViolation(t,
@ -189,6 +196,7 @@ public class BaseAssetService extends AbstractCachedEntityService<AssetCacheKey,
}
publishEvictEvent(new AssetCacheEvictEvent(asset.getTenantId(), asset.getName(), null));
countService.publishCountEntityEvictEvent(tenantId, EntityType.ASSET);
assetDao.removeById(tenantId, assetId.getId());
}
@ -437,6 +445,11 @@ public class BaseAssetService extends AbstractCachedEntityService<AssetCacheKey,
return Optional.ofNullable(findAssetById(tenantId, new AssetId(entityId.getId())));
}
@Override
public long countByTenantId(TenantId tenantId) {
return assetDao.countByTenantId(tenantId);
}
@Override
public EntityType getEntityType() {
return EntityType.ASSET;

17
dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java

@ -35,6 +35,7 @@ import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.entity.EntityCountService;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover;
@ -77,6 +78,9 @@ public class CustomerServiceImpl extends AbstractEntityService implements Custom
@Autowired
private DataValidator<Customer> customerValidator;
@Autowired
private EntityCountService countService;
@Override
public Customer findCustomerById(TenantId tenantId, CustomerId customerId) {
log.trace("Executing findCustomerById [{}]", customerId);
@ -105,6 +109,9 @@ public class CustomerServiceImpl extends AbstractEntityService implements Custom
try {
Customer savedCustomer = customerDao.save(customer.getTenantId(), customer);
dashboardService.updateCustomerDashboards(savedCustomer.getTenantId(), savedCustomer.getId());
if (customer.getId() == null) {
countService.publishCountEntityEvictEvent(savedCustomer.getTenantId(), EntityType.CUSTOMER);
}
return savedCustomer;
} catch (Exception e) {
checkConstraintViolation(e, "customer_external_id_unq_key", "Customer with such external id already exists!");
@ -131,6 +138,7 @@ public class CustomerServiceImpl extends AbstractEntityService implements Custom
deleteEntityRelations(tenantId, customerId);
apiUsageStateService.deleteApiUsageStateByEntityId(customerId);
customerDao.removeById(tenantId, customerId.getId());
countService.publishCountEntityEvictEvent(tenantId, EntityType.CUSTOMER);
}
@Override
@ -149,7 +157,9 @@ public class CustomerServiceImpl extends AbstractEntityService implements Custom
} catch (IOException e) {
throw new IncorrectParameterException("Unable to create public customer.", e);
}
return customerDao.save(tenantId, publicCustomer);
Customer savedCustomer = customerDao.save(tenantId, publicCustomer);
countService.publishCountEntityEvictEvent(tenantId, EntityType.CUSTOMER);
return savedCustomer;
}
}
@ -187,6 +197,11 @@ public class CustomerServiceImpl extends AbstractEntityService implements Custom
return Optional.ofNullable(findCustomerById(tenantId, new CustomerId(entityId.getId())));
}
@Override
public long countByTenantId(TenantId tenantId) {
return customerDao.countByTenantId(tenantId);
}
@Override
public EntityType getEntityType() {
return EntityType.CUSTOMER;

15
dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java

@ -16,6 +16,7 @@
package org.thingsboard.server.dao.dashboard;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.exception.ConstraintViolationException;
import org.springframework.beans.factory.annotation.Autowired;
@ -44,6 +45,7 @@ import org.thingsboard.server.common.data.settings.UserSettingsCompositeKey;
import org.thingsboard.server.dao.customer.CustomerDao;
import org.thingsboard.server.dao.edge.EdgeDao;
import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.entity.EntityCountService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover;
@ -57,6 +59,7 @@ import static org.thingsboard.server.dao.service.Validator.validateId;
@Service("DashboardDaoService")
@Slf4j
@RequiredArgsConstructor
public class DashboardServiceImpl extends AbstractEntityService implements DashboardService {
public static final String INCORRECT_DASHBOARD_ID = "Incorrect dashboardId ";
@ -79,6 +82,9 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb
@Autowired
protected TbTransactionalCache<DashboardId, String> cache;
@Autowired
private EntityCountService countService;
@Autowired
private ApplicationEventPublisher eventPublisher;
@ -136,6 +142,9 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb
try {
var saved = dashboardDao.save(dashboard.getTenantId(), dashboard);
publishEvictEvent(new DashboardTitleEvictEvent(saved.getId()));
if (dashboard.getId() == null) {
countService.publishCountEntityEvictEvent(saved.getTenantId(), EntityType.DASHBOARD);
}
return saved;
} catch (Exception e) {
if (dashboard.getId() != null) {
@ -207,6 +216,7 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb
try {
dashboardDao.removeById(tenantId, dashboardId.getId());
publishEvictEvent(new DashboardTitleEvictEvent(dashboardId));
countService.publishCountEntityEvictEvent(tenantId, EntityType.DASHBOARD);
} catch (Exception t) {
ConstraintViolationException e = extractConstraintViolationException(t).orElse(null);
if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("fk_default_dashboard_device_profile")) {
@ -353,6 +363,11 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb
return Optional.ofNullable(findDashboardById(tenantId, new DashboardId(entityId.getId())));
}
@Override
public long countByTenantId(TenantId tenantId) {
return dashboardDao.countByTenantId(tenantId);
}
@Override
public EntityType getEntityType() {
return EntityType.DASHBOARD;

10
dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java

@ -67,6 +67,7 @@ import org.thingsboard.server.dao.device.provision.ProvisionFailedException;
import org.thingsboard.server.dao.device.provision.ProvisionRequest;
import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus;
import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
import org.thingsboard.server.dao.entity.EntityCountService;
import org.thingsboard.server.dao.event.EventService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.DataValidator;
@ -113,6 +114,9 @@ public class DeviceServiceImpl extends AbstractCachedEntityService<DeviceCacheKe
@Autowired
private DataValidator<Device> deviceValidator;
@Autowired
private EntityCountService countService;
@Override
public DeviceInfo findDeviceInfoById(TenantId tenantId, DeviceId deviceId) {
log.trace("Executing findDeviceInfoById [{}]", deviceId);
@ -235,6 +239,9 @@ public class DeviceServiceImpl extends AbstractCachedEntityService<DeviceCacheKe
device.setDeviceData(syncDeviceData(deviceProfile, device.getDeviceData()));
Device result = deviceDao.saveAndFlush(device.getTenantId(), device);
publishEvictEvent(deviceCacheEvictEvent);
if (device.getId() == null) {
countService.publishCountEntityEvictEvent(result.getTenantId(), EntityType.DEVICE);
}
return result;
} catch (Exception t) {
handleEvictEvent(deviceCacheEvictEvent);
@ -328,9 +335,9 @@ public class DeviceServiceImpl extends AbstractCachedEntityService<DeviceCacheKe
deviceDao.removeById(tenantId, deviceId.getId());
publishEvictEvent(deviceCacheEvictEvent);
countService.publishCountEntityEvictEvent(tenantId, EntityType.DEVICE);
}
@Override
public PageData<Device> findDevicesByTenantId(TenantId tenantId, PageLink pageLink) {
log.trace("Executing findDevicesByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink);
@ -611,6 +618,7 @@ public class DeviceServiceImpl extends AbstractCachedEntityService<DeviceCacheKe
}
publishEvictEvent(new DeviceCacheEvictEvent(savedDevice.getTenantId(), savedDevice.getId(), provisionRequest.getDeviceName(), null));
countService.publishCountEntityEvictEvent(savedDevice.getTenantId(), EntityType.DEVICE);
return savedDevice;
}

49
dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityCountService.java

@ -0,0 +1,49 @@
/**
* Copyright © 2016-2023 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.dao.entity;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.event.TransactionalEventListener;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.TenantId;
@Service
public class BaseEntityCountService extends AbstractCachedEntityService<EntityCountCacheKey, Long, EntityCountCacheEvictEvent> implements EntityCountService {
@Lazy
@Autowired
private EntityServiceRegistry entityServiceRegistry;
@Override
public long countByTenantIdAndEntityType(TenantId tenantId, EntityType entityType) {
return cache.getAndPutInTransaction(new EntityCountCacheKey(tenantId, entityType),
() -> entityServiceRegistry.getServiceByEntityType(entityType).countByTenantId(tenantId), false);
}
@Override
public void publishCountEntityEvictEvent(TenantId tenantId, EntityType entityType) {
publishEvictEvent(new EntityCountCacheEvictEvent(tenantId, entityType));
}
@TransactionalEventListener(classes = EntityCountCacheEvictEvent.class)
@Override
public void handleEvictEvent(EntityCountCacheEvictEvent event) {
cache.evict(new EntityCountCacheKey(event.getTenantId(), event.getEntityType()));
}
}

28
dao/src/main/java/org/thingsboard/server/dao/entity/EntityCountCacheEvictEvent.java

@ -0,0 +1,28 @@
/**
* Copyright © 2016-2023 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.dao.entity;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.TenantId;
@Data
@RequiredArgsConstructor
class EntityCountCacheEvictEvent {
private final TenantId tenantId;
private final EntityType entityType;
}

41
dao/src/main/java/org/thingsboard/server/dao/entity/EntityCountCacheKey.java

@ -0,0 +1,41 @@
/**
* Copyright © 2016-2023 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.dao.entity;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.TenantId;
import java.io.Serializable;
@Getter
@EqualsAndHashCode
@RequiredArgsConstructor
public class EntityCountCacheKey implements Serializable {
private static final long serialVersionUID = -1992105662738434178L;
private final TenantId tenantId;
private final EntityType entityType;
@Override
public String toString() {
return tenantId + "_" + entityType;
}
}

33
dao/src/main/java/org/thingsboard/server/dao/entity/count/EntityCountCaffeineCache.java

@ -0,0 +1,33 @@
/**
* Copyright © 2016-2023 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.dao.entity.count;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CaffeineTbTransactionalCache;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.dao.entity.EntityCountCacheKey;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true)
@Service("EntityCountCache")
public class EntityCountCaffeineCache extends CaffeineTbTransactionalCache<EntityCountCacheKey, Long> {
public EntityCountCaffeineCache(CacheManager cacheManager) {
super(cacheManager, CacheConstants.ENTITY_COUNT_CACHE);
}
}

35
dao/src/main/java/org/thingsboard/server/dao/entity/count/EntityCountRedisCache.java

@ -0,0 +1,35 @@
/**
* Copyright © 2016-2023 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.dao.entity.count;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CacheSpecsMap;
import org.thingsboard.server.cache.RedisTbTransactionalCache;
import org.thingsboard.server.cache.TBRedisCacheConfiguration;
import org.thingsboard.server.cache.TbFSTRedisSerializer;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.dao.entity.EntityCountCacheKey;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis")
@Service("EntityCountCache")
public class EntityCountRedisCache extends RedisTbTransactionalCache<EntityCountCacheKey, Long> {
public EntityCountRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
super(CacheConstants.ENTITY_COUNT_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbFSTRedisSerializer<>());
}
}

98
dao/src/main/java/org/thingsboard/server/dao/usage/BasicUsageInfoService.java

@ -0,0 +1,98 @@
/**
* Copyright © 2016-2023 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.dao.usage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.UsageInfo;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.dao.entity.EntityCountService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.usagerecord.ApiUsageStateService;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
@Service
@Slf4j
@RequiredArgsConstructor
public class BasicUsageInfoService implements UsageInfoService {
private final EntityCountService countService;
private final ApiUsageStateService apiUsageStateService;
private final TimeseriesService tsService;
@Lazy
private final TbTenantProfileCache tenantProfileCache;
@Override
public UsageInfo getUsageInfo(TenantId tenantId) {
DefaultTenantProfileConfiguration profileConfiguration =
(DefaultTenantProfileConfiguration) tenantProfileCache.get(tenantId).getProfileData().getConfiguration();
UsageInfo usageInfo = new UsageInfo();
usageInfo.setDevices(countService.countByTenantIdAndEntityType(tenantId, EntityType.DEVICE));
usageInfo.setMaxDevices(profileConfiguration.getMaxDevices());
usageInfo.setAssets(countService.countByTenantIdAndEntityType(tenantId, EntityType.ASSET));
usageInfo.setMaxAssets(profileConfiguration.getMaxAssets());
usageInfo.setCustomers(countService.countByTenantIdAndEntityType(tenantId, EntityType.CUSTOMER));
usageInfo.setMaxCustomers(profileConfiguration.getMaxCustomers());
usageInfo.setUsers(countService.countByTenantIdAndEntityType(tenantId, EntityType.USER));
usageInfo.setMaxUsers(profileConfiguration.getMaxUsers());
usageInfo.setDashboards(countService.countByTenantIdAndEntityType(tenantId, EntityType.DASHBOARD));
usageInfo.setMaxDashboards(profileConfiguration.getMaxDashboards());
usageInfo.setMaxAlarms(profileConfiguration.getMaxCreatedAlarms());
usageInfo.setMaxTransportMessages(profileConfiguration.getMaxTransportMessages());
usageInfo.setMaxJsExecutions(profileConfiguration.getMaxJSExecutions());
usageInfo.setMaxEmails(profileConfiguration.getMaxEmails());
usageInfo.setMaxSms(profileConfiguration.getMaxSms());
ApiUsageState apiUsageState = apiUsageStateService.findTenantApiUsageState(tenantId);
if (apiUsageState != null) {
Collection<String> keys = Arrays.asList(
ApiUsageRecordKey.TRANSPORT_MSG_COUNT.getApiCountKey(),
ApiUsageRecordKey.JS_EXEC_COUNT.getApiCountKey(),
ApiUsageRecordKey.EMAIL_EXEC_COUNT.getApiCountKey(),
ApiUsageRecordKey.SMS_EXEC_COUNT.getApiCountKey(),
ApiUsageRecordKey.CREATED_ALARMS_COUNT.getApiCountKey());
try {
List<TsKvEntry> entries = tsService.findLatest(tenantId, apiUsageState.getId(), keys).get();
usageInfo.setTransportMessages(getLongValueFromTsEntries(entries, ApiUsageRecordKey.TRANSPORT_MSG_COUNT.getApiCountKey()));
usageInfo.setJsExecutions(getLongValueFromTsEntries(entries, ApiUsageRecordKey.JS_EXEC_COUNT.getApiCountKey()));
usageInfo.setEmails(getLongValueFromTsEntries(entries, ApiUsageRecordKey.EMAIL_EXEC_COUNT.getApiCountKey()));
usageInfo.setSms(getLongValueFromTsEntries(entries, ApiUsageRecordKey.SMS_EXEC_COUNT.getApiCountKey()));
usageInfo.setAlarms(getLongValueFromTsEntries(entries, ApiUsageRecordKey.CREATED_ALARMS_COUNT.getApiCountKey()));
} catch (ExecutionException | InterruptedException e) {
throw new RuntimeException("Failed to fetch api usage values from timeseries!");
}
}
return usageInfo;
}
private long getLongValueFromTsEntries(List<TsKvEntry> entries, String key) {
Optional<TsKvEntry> entryOpt = entries.stream().filter(e -> e.getKey().equals(key)).findFirst();
return entryOpt.map(entry -> entry.getLongValue().orElse(0L)).orElse(0L);
}
}

10
dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java

@ -43,6 +43,7 @@ import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent;
import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.entity.EntityCountService;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover;
@ -79,10 +80,10 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
private final UserDao userDao;
private final UserCredentialsDao userCredentialsDao;
private final UserAuthSettingsDao userAuthSettingsDao;
private final UserSettingsDao userSettingsDao;
private final DataValidator<User> userValidator;
private final DataValidator<UserCredentials> userCredentialsValidator;
private final ApplicationEventPublisher eventPublisher;
private final EntityCountService countService;
@Override
public User findUserByEmail(TenantId tenantId, String email) {
@ -126,6 +127,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
}
User savedUser = userDao.save(user.getTenantId(), user);
if (user.getId() == null) {
countService.publishCountEntityEvictEvent(savedUser.getTenantId(), EntityType.USER);
UserCredentials userCredentials = new UserCredentials();
userCredentials.setEnabled(false);
userCredentials.setActivateToken(generateSafeToken(DEFAULT_TOKEN_LENGTH));
@ -234,6 +236,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
deleteEntityRelations(tenantId, userId);
userDao.removeById(tenantId, userId.getId());
eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(userId));
countService.publishCountEntityEvictEvent(tenantId, EntityType.USER);
}
@Override
@ -431,6 +434,11 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
return Optional.ofNullable(findUserById(tenantId, new UserId(entityId.getId())));
}
@Override
public long countByTenantId(TenantId tenantId) {
return userDao.countByTenantId(tenantId);
}
@Override
public EntityType getEntityType() {
return EntityType.USER;

3
dao/src/test/resources/application-test.properties

@ -71,6 +71,9 @@ cache.specs.notificationRules.maxSize=10000
cache.specs.dashboardTitles.timeToLiveInMinutes=1440
cache.specs.dashboardTitles.maxSize=10000
cache.specs.entityCount.timeToLiveInMinutes=1440
cache.specs.entityCount.maxSize=10000
redis.connection.host=localhost
redis.connection.port=6379
redis.connection.db=0

9
rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java

@ -63,6 +63,7 @@ import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantInfo;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.UpdateMessage;
import org.thingsboard.server.common.data.UsageInfo;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmComment;
@ -2472,6 +2473,14 @@ public class RestClient implements Closeable {
}, params).getBody();
}
public UsageInfo getUsageInfo() {
return restTemplate.exchange(
baseURL + "/api/usage",
HttpMethod.GET,
HttpEntity.EMPTY,
UsageInfo.class).getBody();
}
public Optional<TenantProfile> getTenantProfileById(TenantProfileId tenantProfileId) {
try {
ResponseEntity<TenantProfile> tenantProfile = restTemplate.getForEntity(baseURL + "/api/tenantProfile/{tenantProfileId}", TenantProfile.class, tenantProfileId);

Loading…
Cancel
Save