Commit 29fd4fb02c5446d065e8ba7cf7d095d8fcbf22ee

Authored by vzikratyi
Committed by Andrew Shvayka
1 parent c3a9e691

Save Attributes to cache

... ... @@ -322,6 +322,8 @@ actors:
322 322 cache:
323 323 # caffeine or redis
324 324 type: "${CACHE_TYPE:caffeine}"
  325 + attributes:
  326 + enabled: "${CACHE_ATTRIBUTES_ENABLED:true}"
325 327
326 328 caffeine:
327 329 specs:
... ... @@ -355,6 +357,9 @@ caffeine:
355 357 deviceProfiles:
356 358 timeToLiveInMinutes: 1440
357 359 maxSize: 0
  360 + attributes:
  361 + timeToLiveInMinutes: 1440
  362 + maxSize: 100000
358 363
359 364 redis:
360 365 # standalone or cluster
... ...
... ... @@ -26,4 +26,5 @@ public class CacheConstants {
26 26 public static final String SECURITY_SETTINGS_CACHE = "securitySettings";
27 27 public static final String TENANT_PROFILE_CACHE = "tenantProfiles";
28 28 public static final String DEVICE_PROFILE_CACHE = "deviceProfiles";
  29 + public static final String ATTRIBUTES_CACHE = "attributes";
29 30 }
... ...
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.dao.attributes;
  17 +
  18 +import lombok.AllArgsConstructor;
  19 +import lombok.EqualsAndHashCode;
  20 +import lombok.Getter;
  21 +import org.thingsboard.server.common.data.id.EntityId;
  22 +
  23 +@EqualsAndHashCode
  24 +@Getter
  25 +@AllArgsConstructor
  26 +public class AttributeCacheKey {
  27 + private final String scope;
  28 + private final EntityId entityId;
  29 + private final String key;
  30 +
  31 + @Override
  32 + public String toString() {
  33 + return entityId + "_" + scope + "_" + key;
  34 + }
  35 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.dao.attributes;
  17 +
  18 +import org.thingsboard.server.common.data.id.EntityId;
  19 +import org.thingsboard.server.common.data.kv.AttributeKvEntry;
  20 +import org.thingsboard.server.dao.exception.IncorrectParameterException;
  21 +import org.thingsboard.server.dao.service.Validator;
  22 +
  23 +public class AttributeUtils {
  24 + public static void validate(EntityId id, String scope) {
  25 + Validator.validateId(id.getId(), "Incorrect id " + id);
  26 + Validator.validateString(scope, "Incorrect scope " + scope);
  27 + }
  28 +
  29 + public static void validate(AttributeKvEntry kvEntry) {
  30 + if (kvEntry == null) {
  31 + throw new IncorrectParameterException("Key value entry can't be null");
  32 + } else if (kvEntry.getDataType() == null) {
  33 + throw new IncorrectParameterException("Incorrect kvEntry. Data type can't be null");
  34 + } else {
  35 + Validator.validateString(kvEntry.getKey(), "Incorrect kvEntry. Key can't be empty");
  36 + Validator.validatePositiveNumber(kvEntry.getLastUpdateTs(), "Incorrect last update ts. Ts should be positive");
  37 + }
  38 + }
  39 +}
... ...
... ... @@ -15,31 +15,39 @@
15 15 */
16 16 package org.thingsboard.server.dao.attributes;
17 17
18   -import com.google.common.collect.Lists;
19 18 import com.google.common.util.concurrent.Futures;
20 19 import com.google.common.util.concurrent.ListenableFuture;
21   -import org.springframework.beans.factory.annotation.Autowired;
  20 +import lombok.extern.slf4j.Slf4j;
  21 +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
  22 +import org.springframework.context.annotation.Primary;
22 23 import org.springframework.stereotype.Service;
23 24 import org.thingsboard.server.common.data.EntityType;
24 25 import org.thingsboard.server.common.data.id.DeviceProfileId;
25 26 import org.thingsboard.server.common.data.id.EntityId;
26 27 import org.thingsboard.server.common.data.id.TenantId;
27 28 import org.thingsboard.server.common.data.kv.AttributeKvEntry;
28   -import org.thingsboard.server.dao.exception.IncorrectParameterException;
29 29 import org.thingsboard.server.dao.service.Validator;
30 30
31 31 import java.util.Collection;
32 32 import java.util.List;
33 33 import java.util.Optional;
  34 +import java.util.stream.Collectors;
  35 +
  36 +import static org.thingsboard.server.dao.attributes.AttributeUtils.validate;
34 37
35 38 /**
36 39 * @author Andrew Shvayka
37 40 */
38 41 @Service
  42 +@ConditionalOnProperty(prefix = "cache.attributes", value = "enabled", havingValue = "false", matchIfMissing = true)
  43 +@Primary
  44 +@Slf4j
39 45 public class BaseAttributesService implements AttributesService {
  46 + private final AttributesDao attributesDao;
40 47
41   - @Autowired
42   - private AttributesDao attributesDao;
  48 + public BaseAttributesService(AttributesDao attributesDao) {
  49 + this.attributesDao = attributesDao;
  50 + }
43 51
44 52 @Override
45 53 public ListenableFuture<Optional<AttributeKvEntry>> find(TenantId tenantId, EntityId entityId, String scope, String attributeKey) {
... ... @@ -75,33 +83,14 @@ public class BaseAttributesService implements AttributesService {
75 83 public ListenableFuture<List<Void>> save(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes) {
76 84 validate(entityId, scope);
77 85 attributes.forEach(attribute -> validate(attribute));
78   - List<ListenableFuture<Void>> futures = Lists.newArrayListWithExpectedSize(attributes.size());
79   - for (AttributeKvEntry attribute : attributes) {
80   - futures.add(attributesDao.save(tenantId, entityId, scope, attribute));
81   - }
82   - return Futures.allAsList(futures);
  86 +
  87 + List<ListenableFuture<Void>> saveFutures = attributes.stream().map(attribute -> attributesDao.save(tenantId, entityId, scope, attribute)).collect(Collectors.toList());
  88 + return Futures.allAsList(saveFutures);
83 89 }
84 90
85 91 @Override
86   - public ListenableFuture<List<Void>> removeAll(TenantId tenantId, EntityId entityId, String scope, List<String> keys) {
  92 + public ListenableFuture<List<Void>> removeAll(TenantId tenantId, EntityId entityId, String scope, List<String> attributeKeys) {
87 93 validate(entityId, scope);
88   - return attributesDao.removeAll(tenantId, entityId, scope, keys);
89   - }
90   -
91   - private static void validate(EntityId id, String scope) {
92   - Validator.validateId(id.getId(), "Incorrect id " + id);
93   - Validator.validateString(scope, "Incorrect scope " + scope);
94   - }
95   -
96   - private static void validate(AttributeKvEntry kvEntry) {
97   - if (kvEntry == null) {
98   - throw new IncorrectParameterException("Key value entry can't be null");
99   - } else if (kvEntry.getDataType() == null) {
100   - throw new IncorrectParameterException("Incorrect kvEntry. Data type can't be null");
101   - } else {
102   - Validator.validateString(kvEntry.getKey(), "Incorrect kvEntry. Key can't be empty");
103   - Validator.validatePositiveNumber(kvEntry.getLastUpdateTs(), "Incorrect last update ts. Ts should be positive");
104   - }
  94 + return attributesDao.removeAll(tenantId, entityId, scope, attributeKeys);
105 95 }
106   -
107 96 }
... ...
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.dao.attributes;
  17 +
  18 +import com.google.common.util.concurrent.Futures;
  19 +import com.google.common.util.concurrent.ListenableFuture;
  20 +import com.google.common.util.concurrent.MoreExecutors;
  21 +import lombok.extern.slf4j.Slf4j;
  22 +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
  23 +import org.springframework.cache.Cache;
  24 +import org.springframework.cache.CacheManager;
  25 +import org.springframework.context.annotation.Primary;
  26 +import org.springframework.stereotype.Service;
  27 +import org.thingsboard.server.common.data.EntityType;
  28 +import org.thingsboard.server.common.data.id.DeviceProfileId;
  29 +import org.thingsboard.server.common.data.id.EntityId;
  30 +import org.thingsboard.server.common.data.id.TenantId;
  31 +import org.thingsboard.server.common.data.kv.AttributeKvEntry;
  32 +import org.thingsboard.server.common.data.kv.KvEntry;
  33 +import org.thingsboard.server.common.stats.DefaultCounter;
  34 +import org.thingsboard.server.common.stats.StatsFactory;
  35 +import org.thingsboard.server.dao.service.Validator;
  36 +
  37 +import java.util.ArrayList;
  38 +import java.util.Collection;
  39 +import java.util.HashMap;
  40 +import java.util.List;
  41 +import java.util.Map;
  42 +import java.util.Objects;
  43 +import java.util.Optional;
  44 +import java.util.stream.Collectors;
  45 +
  46 +import static org.thingsboard.server.common.data.CacheConstants.ATTRIBUTES_CACHE;
  47 +import static org.thingsboard.server.dao.attributes.AttributeUtils.validate;
  48 +
  49 +@Service
  50 +@ConditionalOnProperty(prefix = "cache.attributes", value = "enabled", havingValue = "true")
  51 +@Primary
  52 +@Slf4j
  53 +public class CachedAttributesService implements AttributesService {
  54 + private static final String STATS_NAME = "attributes.cache";
  55 +
  56 + private final AttributesDao attributesDao;
  57 + private final Cache attributesCache;
  58 +
  59 + private final DefaultCounter hitCounter;
  60 + private final DefaultCounter missCounter;
  61 +
  62 + public CachedAttributesService(AttributesDao attributesDao,
  63 + CacheManager cacheManager,
  64 + StatsFactory statsFactory) {
  65 + this.attributesDao = attributesDao;
  66 + this.attributesCache = cacheManager.getCache(ATTRIBUTES_CACHE);
  67 +
  68 + this.hitCounter = statsFactory.createDefaultCounter(STATS_NAME, "result", "hit");
  69 + this.missCounter = statsFactory.createDefaultCounter(STATS_NAME, "result", "miss");
  70 + }
  71 +
  72 + @Override
  73 + public ListenableFuture<Optional<AttributeKvEntry>> find(TenantId tenantId, EntityId entityId, String scope, String attributeKey) {
  74 + validate(entityId, scope);
  75 + Validator.validateString(attributeKey, "Incorrect attribute key " + attributeKey);
  76 +
  77 + AttributeCacheKey attributeCacheKey = new AttributeCacheKey(scope, entityId, attributeKey);
  78 + Cache.ValueWrapper cachedAttributeValue = attributesCache.get(attributeCacheKey);
  79 + if (cachedAttributeValue != null) {
  80 + hitCounter.increment();
  81 + AttributeKvEntry cachedAttributeKvEntry = (AttributeKvEntry) cachedAttributeValue.get();
  82 + return Futures.immediateFuture(Optional.ofNullable(cachedAttributeKvEntry));
  83 + } else {
  84 + missCounter.increment();
  85 + ListenableFuture<Optional<AttributeKvEntry>> result = attributesDao.find(tenantId, entityId, scope, attributeKey);
  86 + return Futures.transform(result, foundAttrKvEntry -> {
  87 + // TODO: think if it's a good idea to store 'empty' attributes
  88 + attributesCache.put(attributeKey, foundAttrKvEntry.orElse(null));
  89 + return foundAttrKvEntry;
  90 + }, MoreExecutors.directExecutor());
  91 + }
  92 + }
  93 +
  94 + @Override
  95 + public ListenableFuture<List<AttributeKvEntry>> find(TenantId tenantId, EntityId entityId, String scope, Collection<String> attributeKeys) {
  96 + validate(entityId, scope);
  97 + attributeKeys.forEach(attributeKey -> Validator.validateString(attributeKey, "Incorrect attribute key " + attributeKey));
  98 +
  99 + Map<String, Cache.ValueWrapper> wrappedCachedAttributes = findCachedAttributes(entityId, scope, attributeKeys);
  100 +
  101 + List<AttributeKvEntry> cachedAttributes = wrappedCachedAttributes.values().stream()
  102 + .map(wrappedCachedAttribute -> (AttributeKvEntry) wrappedCachedAttribute.get())
  103 + .filter(Objects::nonNull)
  104 + .collect(Collectors.toList());
  105 + if (wrappedCachedAttributes.size() == attributeKeys.size()) {
  106 + return Futures.immediateFuture(cachedAttributes);
  107 + }
  108 +
  109 + ArrayList<String> notFoundAttributeKeys = new ArrayList<>(attributeKeys);
  110 + notFoundAttributeKeys.removeAll(wrappedCachedAttributes.keySet());
  111 +
  112 + ListenableFuture<List<AttributeKvEntry>> result = attributesDao.find(tenantId, entityId, scope, notFoundAttributeKeys);
  113 + return Futures.transform(result, foundInDbAttributes -> {
  114 + return mergeDbAndCacheAttributes(entityId, scope, cachedAttributes, notFoundAttributeKeys, foundInDbAttributes);
  115 + }, MoreExecutors.directExecutor());
  116 +
  117 + }
  118 +
  119 + private Map<String, Cache.ValueWrapper> findCachedAttributes(EntityId entityId, String scope, Collection<String> attributeKeys) {
  120 + Map<String, Cache.ValueWrapper> cachedAttributes = new HashMap<>();
  121 + for (String attributeKey : attributeKeys) {
  122 + Cache.ValueWrapper cachedAttributeValue = attributesCache.get(new AttributeCacheKey(scope, entityId, attributeKey));
  123 + if (cachedAttributeValue != null) {
  124 + hitCounter.increment();
  125 + cachedAttributes.put(attributeKey, cachedAttributeValue);
  126 + } else {
  127 + missCounter.increment();
  128 + }
  129 + }
  130 + return cachedAttributes;
  131 + }
  132 +
  133 + private List<AttributeKvEntry> mergeDbAndCacheAttributes(EntityId entityId, String scope, List<AttributeKvEntry> cachedAttributes, ArrayList<String> notFoundAttributeKeys, List<AttributeKvEntry> foundInDbAttributes) {
  134 + for (AttributeKvEntry foundInDbAttribute : foundInDbAttributes) {
  135 + AttributeCacheKey attributeCacheKey = new AttributeCacheKey(scope, entityId, foundInDbAttribute.getKey());
  136 + attributesCache.put(attributeCacheKey, foundInDbAttribute);
  137 + notFoundAttributeKeys.remove(foundInDbAttribute.getKey());
  138 + }
  139 + for (String key : notFoundAttributeKeys){
  140 + attributesCache.put(new AttributeCacheKey(scope, entityId, key), null);
  141 + }
  142 + List<AttributeKvEntry> mergedAttributes = new ArrayList<>(cachedAttributes);
  143 + mergedAttributes.addAll(foundInDbAttributes);
  144 + return mergedAttributes;
  145 + }
  146 +
  147 + @Override
  148 + public ListenableFuture<List<AttributeKvEntry>> findAll(TenantId tenantId, EntityId entityId, String scope) {
  149 + validate(entityId, scope);
  150 + return attributesDao.findAll(tenantId, entityId, scope);
  151 + }
  152 +
  153 + @Override
  154 + public List<String> findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId) {
  155 + return attributesDao.findAllKeysByDeviceProfileId(tenantId, deviceProfileId);
  156 + }
  157 +
  158 + @Override
  159 + public List<String> findAllKeysByEntityIds(TenantId tenantId, EntityType entityType, List<EntityId> entityIds) {
  160 + return attributesDao.findAllKeysByEntityIds(tenantId, entityType, entityIds);
  161 + }
  162 +
  163 + @Override
  164 + public ListenableFuture<List<Void>> save(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes) {
  165 + validate(entityId, scope);
  166 + attributes.forEach(AttributeUtils::validate);
  167 +
  168 + List<ListenableFuture<Void>> saveFutures = attributes.stream().map(attribute -> attributesDao.save(tenantId, entityId, scope, attribute)).collect(Collectors.toList());
  169 + ListenableFuture<List<Void>> future = Futures.allAsList(saveFutures);
  170 +
  171 + // TODO: can do if (attributesCache.get() != null) attributesCache.put() instead, but will be more twice more requests to cache
  172 + List<String> attributeKeys = attributes.stream().map(KvEntry::getKey).collect(Collectors.toList());
  173 + future.addListener(() -> evictAttributesFromCache(tenantId, entityId, scope, attributeKeys), MoreExecutors.directExecutor());
  174 + return future;
  175 + }
  176 +
  177 + @Override
  178 + public ListenableFuture<List<Void>> removeAll(TenantId tenantId, EntityId entityId, String scope, List<String> attributeKeys) {
  179 + validate(entityId, scope);
  180 + ListenableFuture<List<Void>> future = attributesDao.removeAll(tenantId, entityId, scope, attributeKeys);
  181 + future.addListener(() -> evictAttributesFromCache(tenantId, entityId, scope, attributeKeys), MoreExecutors.directExecutor());
  182 + return future;
  183 + }
  184 +
  185 + private void evictAttributesFromCache(TenantId tenantId, EntityId entityId, String scope, List<String> attributeKeys) {
  186 + try {
  187 + for (String attributeKey : attributeKeys) {
  188 + attributesCache.evict(new AttributeCacheKey(scope, entityId, attributeKey));
  189 + }
  190 + } catch (Exception e) {
  191 + log.error("[{}][{}] Failed to remove values from cache.", tenantId, entityId, e);
  192 + }
  193 + }
  194 +}
... ...