Commit b67f454ba04110e338e23cd37e2a438d889d7c78

Authored by Igor Kulikov
1 parent 0d7adb73

Handle Device service transactional methods exceptions (fix exception handling f…

…or devices with same name)
@@ -30,7 +30,6 @@ import org.springframework.cache.annotation.Cacheable; @@ -30,7 +30,6 @@ import org.springframework.cache.annotation.Cacheable;
30 import org.springframework.cache.annotation.Caching; 30 import org.springframework.cache.annotation.Caching;
31 import org.springframework.context.annotation.Lazy; 31 import org.springframework.context.annotation.Lazy;
32 import org.springframework.stereotype.Service; 32 import org.springframework.stereotype.Service;
33 -import org.springframework.transaction.annotation.Transactional;  
34 import org.springframework.util.CollectionUtils; 33 import org.springframework.util.CollectionUtils;
35 import org.springframework.util.StringUtils; 34 import org.springframework.util.StringUtils;
36 import org.thingsboard.common.util.JacksonUtil; 35 import org.thingsboard.common.util.JacksonUtil;
@@ -82,6 +81,7 @@ import org.thingsboard.server.dao.service.DataValidator; @@ -82,6 +81,7 @@ import org.thingsboard.server.dao.service.DataValidator;
82 import org.thingsboard.server.dao.service.PaginatedRemover; 81 import org.thingsboard.server.dao.service.PaginatedRemover;
83 import org.thingsboard.server.dao.tenant.TbTenantProfileCache; 82 import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
84 import org.thingsboard.server.dao.tenant.TenantDao; 83 import org.thingsboard.server.dao.tenant.TenantDao;
  84 +import org.thingsboard.server.dao.tx.TransactionHandler;
85 85
86 import javax.annotation.Nullable; 86 import javax.annotation.Nullable;
87 import java.util.ArrayList; 87 import java.util.ArrayList;
@@ -141,6 +141,9 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe @@ -141,6 +141,9 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
141 @Autowired 141 @Autowired
142 private OtaPackageService otaPackageService; 142 private OtaPackageService otaPackageService;
143 143
  144 + @Autowired
  145 + private TransactionHandler transactionHandler;
  146 +
144 @Override 147 @Override
145 public DeviceInfo findDeviceInfoById(TenantId tenantId, DeviceId deviceId) { 148 public DeviceInfo findDeviceInfoById(TenantId tenantId, DeviceId deviceId) {
146 log.trace("Executing findDeviceInfoById [{}]", deviceId); 149 log.trace("Executing findDeviceInfoById [{}]", deviceId);
@@ -184,10 +187,13 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe @@ -184,10 +187,13 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
184 @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.name}"), 187 @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.name}"),
185 @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.id}") 188 @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.id}")
186 }) 189 })
187 - @Transactional  
188 @Override 190 @Override
189 public Device saveDeviceWithAccessToken(Device device, String accessToken) { 191 public Device saveDeviceWithAccessToken(Device device, String accessToken) {
190 - return doSaveDevice(device, accessToken, true); 192 + try {
  193 + return transactionHandler.runInTransaction(() -> doSaveDevice(device, accessToken, true));
  194 + } catch (Exception t) {
  195 + throw handleDeviceSaveException(device, t);
  196 + }
191 } 197 }
192 198
193 @Caching(evict= { 199 @Caching(evict= {
@@ -212,26 +218,32 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe @@ -212,26 +218,32 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
212 @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.name}"), 218 @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.name}"),
213 @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.id}") 219 @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.id}")
214 }) 220 })
215 - @Transactional  
216 @Override 221 @Override
217 - public Device saveDeviceWithCredentials(Device device, DeviceCredentials deviceCredentials) {  
218 - if (device.getId() == null) {  
219 - Device deviceWithName = this.findDeviceByTenantIdAndName(device.getTenantId(), device.getName());  
220 - device = deviceWithName == null ? device : deviceWithName.updateDevice(device);  
221 - }  
222 - Device savedDevice = this.saveDeviceWithoutCredentials(device, true);  
223 - deviceCredentials.setDeviceId(savedDevice.getId());  
224 - if (device.getId() == null) {  
225 - deviceCredentialsService.createDeviceCredentials(savedDevice.getTenantId(), deviceCredentials);  
226 - } else {  
227 - DeviceCredentials foundDeviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getTenantId(), savedDevice.getId());  
228 - if (foundDeviceCredentials == null) {  
229 - deviceCredentialsService.createDeviceCredentials(savedDevice.getTenantId(), deviceCredentials);  
230 - } else {  
231 - deviceCredentialsService.updateDeviceCredentials(device.getTenantId(), deviceCredentials);  
232 - } 222 + public Device saveDeviceWithCredentials(Device toSave, DeviceCredentials deviceCredentials) {
  223 + try {
  224 + return transactionHandler.runInTransaction(() -> {
  225 + Device device = toSave;
  226 + if (device.getId() == null) {
  227 + Device deviceWithName = this.findDeviceByTenantIdAndName(device.getTenantId(), device.getName());
  228 + device = deviceWithName == null ? device : deviceWithName.updateDevice(device);
  229 + }
  230 + Device savedDevice = this.saveDeviceWithoutCredentials(device, true);
  231 + deviceCredentials.setDeviceId(savedDevice.getId());
  232 + if (device.getId() == null) {
  233 + deviceCredentialsService.createDeviceCredentials(savedDevice.getTenantId(), deviceCredentials);
  234 + } else {
  235 + DeviceCredentials foundDeviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getTenantId(), savedDevice.getId());
  236 + if (foundDeviceCredentials == null) {
  237 + deviceCredentialsService.createDeviceCredentials(savedDevice.getTenantId(), deviceCredentials);
  238 + } else {
  239 + deviceCredentialsService.updateDeviceCredentials(device.getTenantId(), deviceCredentials);
  240 + }
  241 + }
  242 + return savedDevice;
  243 + });
  244 + } catch (Exception t) {
  245 + throw handleDeviceSaveException(toSave, t);
233 } 246 }
234 - return savedDevice;  
235 } 247 }
236 248
237 private Device doSaveDevice(Device device, String accessToken, boolean doValidate) { 249 private Device doSaveDevice(Device device, String accessToken, boolean doValidate) {
@@ -270,15 +282,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe @@ -270,15 +282,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
270 device.setDeviceData(syncDeviceData(deviceProfile, device.getDeviceData())); 282 device.setDeviceData(syncDeviceData(deviceProfile, device.getDeviceData()));
271 return deviceDao.save(device.getTenantId(), device); 283 return deviceDao.save(device.getTenantId(), device);
272 } catch (Exception t) { 284 } catch (Exception t) {
273 - ConstraintViolationException e = extractConstraintViolationException(t).orElse(null);  
274 - if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("device_name_unq_key")) {  
275 - // remove device from cache in case null value cached in the distributed redis.  
276 - removeDeviceFromCacheByName(device.getTenantId(), device.getName());  
277 - removeDeviceFromCacheById(device.getTenantId(), device.getId());  
278 - throw new DataValidationException("Device with such name already exists!");  
279 - } else {  
280 - throw t;  
281 - } 285 + throw handleDeviceSaveException(device, t);
282 } 286 }
283 } 287 }
284 288
@@ -364,16 +368,17 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe @@ -364,16 +368,17 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
364 } 368 }
365 369
366 private void removeDeviceFromCacheByName(TenantId tenantId, String name) { 370 private void removeDeviceFromCacheByName(TenantId tenantId, String name) {
367 - Cache cache = cacheManager.getCache(DEVICE_CACHE);  
368 - cache.evict(Arrays.asList(tenantId, name)); 371 + if (tenantId != null && !StringUtils.isEmpty(name)) {
  372 + Cache cache = cacheManager.getCache(DEVICE_CACHE);
  373 + cache.evict(Arrays.asList(tenantId, name));
  374 + }
369 } 375 }
370 376
371 private void removeDeviceFromCacheById(TenantId tenantId, DeviceId deviceId) { 377 private void removeDeviceFromCacheById(TenantId tenantId, DeviceId deviceId) {
372 - if (deviceId == null) {  
373 - return; 378 + if (tenantId != null && deviceId != null) {
  379 + Cache cache = cacheManager.getCache(DEVICE_CACHE);
  380 + cache.evict(Arrays.asList(tenantId, deviceId));
374 } 381 }
375 - Cache cache = cacheManager.getCache(DEVICE_CACHE);  
376 - cache.evict(Arrays.asList(tenantId, deviceId));  
377 } 382 }
378 383
379 @Override 384 @Override
@@ -560,84 +565,93 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe @@ -560,84 +565,93 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
560 }, MoreExecutors.directExecutor()); 565 }, MoreExecutors.directExecutor());
561 } 566 }
562 567
563 - @Transactional  
564 @Override 568 @Override
565 public Device assignDeviceToTenant(TenantId tenantId, Device device) { 569 public Device assignDeviceToTenant(TenantId tenantId, Device device) {
566 log.trace("Executing assignDeviceToTenant [{}][{}]", tenantId, device); 570 log.trace("Executing assignDeviceToTenant [{}][{}]", tenantId, device);
567 -  
568 try { 571 try {
569 - List<EntityView> entityViews = entityViewService.findEntityViewsByTenantIdAndEntityIdAsync(device.getTenantId(), device.getId()).get();  
570 - if (!CollectionUtils.isEmpty(entityViews)) {  
571 - throw new DataValidationException("Can't assign device that has entity views to another tenant!");  
572 - }  
573 - } catch (ExecutionException | InterruptedException e) {  
574 - log.error("Exception while finding entity views for deviceId [{}]", device.getId(), e);  
575 - throw new RuntimeException("Exception while finding entity views for deviceId [" + device.getId() + "]", e);  
576 - } 572 + return transactionHandler.runInTransaction(() -> {
  573 + try {
  574 + List<EntityView> entityViews = entityViewService.findEntityViewsByTenantIdAndEntityIdAsync(device.getTenantId(), device.getId()).get();
  575 + if (!CollectionUtils.isEmpty(entityViews)) {
  576 + throw new DataValidationException("Can't assign device that has entity views to another tenant!");
  577 + }
  578 + } catch (ExecutionException | InterruptedException e) {
  579 + log.error("Exception while finding entity views for deviceId [{}]", device.getId(), e);
  580 + throw new RuntimeException("Exception while finding entity views for deviceId [" + device.getId() + "]", e);
  581 + }
577 582
578 - eventService.removeEvents(device.getTenantId(), device.getId()); 583 + eventService.removeEvents(device.getTenantId(), device.getId());
579 584
580 - relationService.removeRelations(device.getTenantId(), device.getId()); 585 + relationService.removeRelations(device.getTenantId(), device.getId());
581 586
582 - TenantId oldTenantId = device.getTenantId(); 587 + TenantId oldTenantId = device.getTenantId();
583 588
584 - device.setTenantId(tenantId);  
585 - device.setCustomerId(null);  
586 - Device savedDevice = doSaveDevice(device, null, true); 589 + device.setTenantId(tenantId);
  590 + device.setCustomerId(null);
  591 + Device savedDevice = doSaveDevice(device, null, true);
587 592
588 - // explicitly remove device with previous tenant id from cache  
589 - // result device object will have different tenant id and will not remove entity from cache  
590 - removeDeviceFromCacheByName(oldTenantId, device.getName());  
591 - removeDeviceFromCacheById(oldTenantId, device.getId()); 593 + // explicitly remove device with previous tenant id from cache
  594 + // result device object will have different tenant id and will not remove entity from cache
  595 + removeDeviceFromCacheByName(oldTenantId, device.getName());
  596 + removeDeviceFromCacheById(oldTenantId, device.getId());
592 597
593 - return savedDevice; 598 + return savedDevice;
  599 + });
  600 + } catch (Exception t) {
  601 + throw handleDeviceSaveException(device, t);
  602 + }
594 } 603 }
595 604
596 @Override 605 @Override
597 @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#profile.tenantId, #provisionRequest.deviceName}") 606 @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#profile.tenantId, #provisionRequest.deviceName}")
598 - @Transactional  
599 public Device saveDevice(ProvisionRequest provisionRequest, DeviceProfile profile) { 607 public Device saveDevice(ProvisionRequest provisionRequest, DeviceProfile profile) {
600 Device device = new Device(); 608 Device device = new Device();
601 - device.setName(provisionRequest.getDeviceName());  
602 - device.setType(profile.getName());  
603 - device.setTenantId(profile.getTenantId());  
604 - Device savedDevice = saveDevice(device);  
605 - if (!StringUtils.isEmpty(provisionRequest.getCredentialsData().getToken()) ||  
606 - !StringUtils.isEmpty(provisionRequest.getCredentialsData().getX509CertHash()) ||  
607 - !StringUtils.isEmpty(provisionRequest.getCredentialsData().getUsername()) ||  
608 - !StringUtils.isEmpty(provisionRequest.getCredentialsData().getPassword()) ||  
609 - !StringUtils.isEmpty(provisionRequest.getCredentialsData().getClientId())) {  
610 - DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedDevice.getTenantId(), savedDevice.getId());  
611 - if (deviceCredentials == null) {  
612 - deviceCredentials = new DeviceCredentials();  
613 - }  
614 - deviceCredentials.setDeviceId(savedDevice.getId());  
615 - deviceCredentials.setCredentialsType(provisionRequest.getCredentialsType());  
616 - switch (provisionRequest.getCredentialsType()) {  
617 - case ACCESS_TOKEN:  
618 - deviceCredentials.setCredentialsId(provisionRequest.getCredentialsData().getToken());  
619 - break;  
620 - case MQTT_BASIC:  
621 - BasicMqttCredentials mqttCredentials = new BasicMqttCredentials();  
622 - mqttCredentials.setClientId(provisionRequest.getCredentialsData().getClientId());  
623 - mqttCredentials.setUserName(provisionRequest.getCredentialsData().getUsername());  
624 - mqttCredentials.setPassword(provisionRequest.getCredentialsData().getPassword());  
625 - deviceCredentials.setCredentialsValue(JacksonUtil.toString(mqttCredentials));  
626 - break;  
627 - case X509_CERTIFICATE:  
628 - deviceCredentials.setCredentialsValue(provisionRequest.getCredentialsData().getX509CertHash());  
629 - break;  
630 - case LWM2M_CREDENTIALS:  
631 - break;  
632 - }  
633 - try {  
634 - deviceCredentialsService.updateDeviceCredentials(savedDevice.getTenantId(), deviceCredentials);  
635 - } catch (Exception e) {  
636 - throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name());  
637 - } 609 + try {
  610 + return transactionHandler.runInTransaction(() -> {
  611 + device.setName(provisionRequest.getDeviceName());
  612 + device.setType(profile.getName());
  613 + device.setTenantId(profile.getTenantId());
  614 + Device savedDevice = saveDevice(device);
  615 + if (!StringUtils.isEmpty(provisionRequest.getCredentialsData().getToken()) ||
  616 + !StringUtils.isEmpty(provisionRequest.getCredentialsData().getX509CertHash()) ||
  617 + !StringUtils.isEmpty(provisionRequest.getCredentialsData().getUsername()) ||
  618 + !StringUtils.isEmpty(provisionRequest.getCredentialsData().getPassword()) ||
  619 + !StringUtils.isEmpty(provisionRequest.getCredentialsData().getClientId())) {
  620 + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedDevice.getTenantId(), savedDevice.getId());
  621 + if (deviceCredentials == null) {
  622 + deviceCredentials = new DeviceCredentials();
  623 + }
  624 + deviceCredentials.setDeviceId(savedDevice.getId());
  625 + deviceCredentials.setCredentialsType(provisionRequest.getCredentialsType());
  626 + switch (provisionRequest.getCredentialsType()) {
  627 + case ACCESS_TOKEN:
  628 + deviceCredentials.setCredentialsId(provisionRequest.getCredentialsData().getToken());
  629 + break;
  630 + case MQTT_BASIC:
  631 + BasicMqttCredentials mqttCredentials = new BasicMqttCredentials();
  632 + mqttCredentials.setClientId(provisionRequest.getCredentialsData().getClientId());
  633 + mqttCredentials.setUserName(provisionRequest.getCredentialsData().getUsername());
  634 + mqttCredentials.setPassword(provisionRequest.getCredentialsData().getPassword());
  635 + deviceCredentials.setCredentialsValue(JacksonUtil.toString(mqttCredentials));
  636 + break;
  637 + case X509_CERTIFICATE:
  638 + deviceCredentials.setCredentialsValue(provisionRequest.getCredentialsData().getX509CertHash());
  639 + break;
  640 + case LWM2M_CREDENTIALS:
  641 + break;
  642 + }
  643 + try {
  644 + deviceCredentialsService.updateDeviceCredentials(savedDevice.getTenantId(), deviceCredentials);
  645 + } catch (Exception e) {
  646 + throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name());
  647 + }
  648 + }
  649 + removeDeviceFromCacheById(savedDevice.getTenantId(), savedDevice.getId()); // eviction by name is described as annotation @CacheEvict above
  650 + return savedDevice;
  651 + });
  652 + } catch (Exception t) {
  653 + throw handleDeviceSaveException(device, t);
638 } 654 }
639 - removeDeviceFromCacheById(savedDevice.getTenantId(), savedDevice.getId()); // eviction by name is described as annotation @CacheEvict above  
640 - return savedDevice;  
641 } 655 }
642 656
643 @Override 657 @Override
@@ -818,4 +832,20 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe @@ -818,4 +832,20 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
818 unassignDeviceFromCustomer(tenantId, new DeviceId(entity.getUuidId())); 832 unassignDeviceFromCustomer(tenantId, new DeviceId(entity.getUuidId()));
819 } 833 }
820 }; 834 };
  835 +
  836 + private RuntimeException handleDeviceSaveException(Device device, Exception t) {
  837 + ConstraintViolationException e = extractConstraintViolationException(t).orElse(null);
  838 + if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("device_name_unq_key")) {
  839 + // remove device from cache in case null value cached in the distributed redis.
  840 + if (device != null) {
  841 + removeDeviceFromCacheByName(device.getTenantId(), device.getName());
  842 + removeDeviceFromCacheById(device.getTenantId(), device.getId());
  843 + }
  844 + return new DataValidationException("Device with such name already exists!");
  845 + } else if (t instanceof RuntimeException) {
  846 + return (RuntimeException)t;
  847 + } else {
  848 + return new RuntimeException("Failed to save device!", t);
  849 + }
  850 + }
821 } 851 }
  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.tx;
  17 +
  18 +import org.springframework.stereotype.Service;
  19 +import org.springframework.transaction.annotation.Propagation;
  20 +import org.springframework.transaction.annotation.Transactional;
  21 +
  22 +import java.util.function.Supplier;
  23 +
  24 +@Service
  25 +public class TransactionHandler {
  26 +
  27 + @Transactional(propagation = Propagation.REQUIRED)
  28 + public <T> T runInTransaction(Supplier<T> supplier) {
  29 + return supplier.get();
  30 + }
  31 +
  32 + @Transactional(propagation = Propagation.REQUIRES_NEW)
  33 + public <T> T runInNewTransaction(Supplier<T> supplier) {
  34 + return supplier.get();
  35 + }
  36 +}