From bb8af6cb220090f691d81964fc983bd190320a85 Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Fri, 17 Nov 2023 19:05:14 +0200 Subject: [PATCH 1/2] Local ETag cache --- .../server/controller/ImageController.java | 49 +++++++++++-------- .../resource/DefaultTbImageService.java | 29 ++++++++++- .../service/resource/ImageCacheKey.java | 13 +++++ .../service/resource/TbImageService.java | 3 ++ .../src/main/resources/thingsboard.yml | 3 ++ 5 files changed, 74 insertions(+), 23 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/resource/ImageCacheKey.java diff --git a/application/src/main/java/org/thingsboard/server/controller/ImageController.java b/application/src/main/java/org/thingsboard/server/controller/ImageController.java index 98e7a3a4e8..2ae358ff15 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ImageController.java +++ b/application/src/main/java/org/thingsboard/server/controller/ImageController.java @@ -5,7 +5,7 @@ * 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, @@ -15,6 +15,7 @@ */ package org.thingsboard.server.controller; +import com.fasterxml.jackson.core.JsonProcessingException; import io.swagger.annotations.ApiParam; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -37,6 +38,7 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import org.thingsboard.server.common.data.ImageDescriptor; import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.exception.ThingsboardException; @@ -46,11 +48,13 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.resource.ImageCacheKey; import org.thingsboard.server.service.resource.TbImageService; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; +import java.time.Duration; import java.util.function.Supplier; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; @@ -122,11 +126,7 @@ public class ImageController extends BaseController { public ResponseEntity downloadImage(@PathVariable String type, @PathVariable String key, @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws Exception { - TenantId tenantId = getTenantId(); - TbResourceInfo imageInfo = checkImageInfo(type, key, Operation.READ); - ImageDescriptor descriptor = imageInfo.getDescriptor(ImageDescriptor.class); - return downloadIfChanged(etag, descriptor, imageInfo.getFileName(), - () -> imageService.getImageData(tenantId, imageInfo.getId())); + return downloadIfChanged(type, key, etag, false); } @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @@ -134,11 +134,7 @@ public class ImageController extends BaseController { public ResponseEntity downloadImagePreview(@PathVariable String type, @PathVariable String key, @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws Exception { - TenantId tenantId = getTenantId(); - TbResourceInfo imageInfo = checkImageInfo(type, key, Operation.READ); - ImageDescriptor descriptor = imageInfo.getDescriptor(ImageDescriptor.class); - return downloadIfChanged(etag, descriptor.getPreviewDescriptor(), imageInfo.getFileName(), - () -> imageService.getImagePreview(tenantId, imageInfo.getId())); + return downloadIfChanged(type, key, etag, true); } @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @@ -178,22 +174,33 @@ public class ImageController extends BaseController { tbImageService.delete(imageInfo, getCurrentUser()); } - private ResponseEntity downloadIfChanged(String actualEtag, ImageDescriptor imageDescriptor, - String fileName, Supplier dataSupplier) { - if (imageDescriptor.getEtag().equals(actualEtag)) { - return ResponseEntity.status(HttpStatus.NOT_MODIFIED) - .eTag(actualEtag) - .build(); + private ResponseEntity downloadIfChanged(String type, String key, String etag, boolean preview) throws ThingsboardException, JsonProcessingException { + ImageCacheKey cacheKey = new ImageCacheKey(getTenantId(type), key, preview); + if (StringUtils.isNotEmpty(etag)) { + etag = etag.replaceAll("\"", ""); // TODO: investigate why Spring provides extra quotes. + if (etag.equals(tbImageService.getETag(cacheKey))) { + return ResponseEntity.status(HttpStatus.NOT_MODIFIED).build(); + } } - - byte[] data = dataSupplier.get(); + TenantId tenantId = getTenantId(); + TbResourceInfo imageInfo = checkImageInfo(type, key, Operation.READ); + String fileName = imageInfo.getFileName(); + ImageDescriptor descriptor = imageInfo.getDescriptor(ImageDescriptor.class); + byte[] data; + if (preview) { + descriptor = descriptor.getPreviewDescriptor(); + data = imageService.getImagePreview(tenantId, imageInfo.getId()); + } else { + data = imageService.getImageData(tenantId, imageInfo.getId()); + } + tbImageService.putETag(cacheKey, descriptor.getEtag()); return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + fileName) .header("x-filename", fileName) .contentLength(data.length) - .header("Content-Type", imageDescriptor.getMediaType()) + .header("Content-Type", descriptor.getMediaType()) .cacheControl(CacheControl.noCache()) - .eTag(imageDescriptor.getEtag()) + .eTag(descriptor.getEtag()) .body(new ByteArrayResource(data)); } diff --git a/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbImageService.java b/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbImageService.java index 65304c8270..5ded1d7fb5 100644 --- a/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbImageService.java +++ b/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbImageService.java @@ -5,7 +5,7 @@ * 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, @@ -15,7 +15,10 @@ */ package org.thingsboard.server.service.resource; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.TbResource; @@ -28,12 +31,34 @@ import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; +import java.util.concurrent.TimeUnit; + @Service @TbCoreComponent -@RequiredArgsConstructor public class DefaultTbImageService extends AbstractTbEntityService implements TbImageService { private final ImageService imageService; + private final Cache cache; + + public DefaultTbImageService(ImageService imageService, + @Value("${cache.imageETags.timeToLiveInMinutes:120}") int cacheTtl, + @Value("${cache.imageETags.maxSize:200000}") int cacheMaxSize) { + this.imageService = imageService; + this.cache = Caffeine.newBuilder() + .expireAfterAccess(cacheTtl, TimeUnit.MINUTES) + .maximumSize(cacheMaxSize) + .build(); + } + + @Override + public String getETag(ImageCacheKey imageCacheKey) { + return cache.getIfPresent(imageCacheKey); + } + + @Override + public void putETag(ImageCacheKey imageCacheKey, String etag) { + cache.put(imageCacheKey, etag); + } @Override public TbResourceInfo save(TbResource image, User user) throws Exception { diff --git a/application/src/main/java/org/thingsboard/server/service/resource/ImageCacheKey.java b/application/src/main/java/org/thingsboard/server/service/resource/ImageCacheKey.java new file mode 100644 index 0000000000..222fc5e875 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/resource/ImageCacheKey.java @@ -0,0 +1,13 @@ +package org.thingsboard.server.service.resource; + +import lombok.Data; +import org.thingsboard.server.common.data.id.TenantId; + +@Data +public class ImageCacheKey { + + private final TenantId tenantId; + private final String key; + private final boolean preview; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/resource/TbImageService.java b/application/src/main/java/org/thingsboard/server/service/resource/TbImageService.java index a1f6cf3e7e..3db512528a 100644 --- a/application/src/main/java/org/thingsboard/server/service/resource/TbImageService.java +++ b/application/src/main/java/org/thingsboard/server/service/resource/TbImageService.java @@ -27,4 +27,7 @@ public interface TbImageService { void delete(TbResourceInfo imageInfo, User user); + String getETag(ImageCacheKey imageCacheKey); + + void putETag(ImageCacheKey imageCacheKey, String etag); } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index ecc89c84c5..9e59db76e8 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -586,6 +586,9 @@ cache: entityLimits: timeToLiveInMinutes: "${CACHE_SPECS_ENTITY_LIMITS_TTL:5}" # Entity limits cache TTL maxSize: "${CACHE_SPECS_ENTITY_LIMITS_MAX_SIZE:100000}" # 0 means the cache is disabled + imageETags: + timeToLiveInMinutes: "${CACHE_SPECS_IMAGE_ETAGS_TTL:44640}" # Image ETags cache TTL + maxSize: "${CACHE_SPECS_IMAGE_ETAGS_MAX_SIZE:1000000}" # 0 means the cache is disabled # Spring data parameters spring.data.redis.repositories.enabled: false # Disable this because it is not required. From 0051abee3446a8264ab0a2217fa9cc30b1e081cd Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Fri, 17 Nov 2023 19:06:14 +0200 Subject: [PATCH 2/2] Local ETag cache --- .../server/controller/ImageController.java | 2 +- .../service/resource/DefaultTbImageService.java | 2 +- .../server/service/resource/ImageCacheKey.java | 15 +++++++++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/ImageController.java b/application/src/main/java/org/thingsboard/server/controller/ImageController.java index 2ae358ff15..a027e77b2e 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ImageController.java +++ b/application/src/main/java/org/thingsboard/server/controller/ImageController.java @@ -5,7 +5,7 @@ * 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, diff --git a/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbImageService.java b/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbImageService.java index 5ded1d7fb5..4b70814e26 100644 --- a/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbImageService.java +++ b/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbImageService.java @@ -5,7 +5,7 @@ * 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, diff --git a/application/src/main/java/org/thingsboard/server/service/resource/ImageCacheKey.java b/application/src/main/java/org/thingsboard/server/service/resource/ImageCacheKey.java index 222fc5e875..55ddb25dec 100644 --- a/application/src/main/java/org/thingsboard/server/service/resource/ImageCacheKey.java +++ b/application/src/main/java/org/thingsboard/server/service/resource/ImageCacheKey.java @@ -1,3 +1,18 @@ +/** + * 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.service.resource; import lombok.Data;