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 30 import org.springframework.cache.annotation.Caching;
31 31 import org.springframework.context.annotation.Lazy;
32 32 import org.springframework.stereotype.Service;
33   -import org.springframework.transaction.annotation.Transactional;
34 33 import org.springframework.util.CollectionUtils;
35 34 import org.springframework.util.StringUtils;
36 35 import org.thingsboard.common.util.JacksonUtil;
... ... @@ -82,6 +81,7 @@ import org.thingsboard.server.dao.service.DataValidator;
82 81 import org.thingsboard.server.dao.service.PaginatedRemover;
83 82 import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
84 83 import org.thingsboard.server.dao.tenant.TenantDao;
  84 +import org.thingsboard.server.dao.tx.TransactionHandler;
85 85
86 86 import javax.annotation.Nullable;
87 87 import java.util.ArrayList;
... ... @@ -141,6 +141,9 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
141 141 @Autowired
142 142 private OtaPackageService otaPackageService;
143 143
  144 + @Autowired
  145 + private TransactionHandler transactionHandler;
  146 +
144 147 @Override
145 148 public DeviceInfo findDeviceInfoById(TenantId tenantId, DeviceId deviceId) {
146 149 log.trace("Executing findDeviceInfoById [{}]", deviceId);
... ... @@ -184,10 +187,13 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
184 187 @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.name}"),
185 188 @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.id}")
186 189 })
187   - @Transactional
188 190 @Override
189 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 199 @Caching(evict= {
... ... @@ -212,26 +218,32 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
212 218 @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.name}"),
213 219 @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.id}")
214 220 })
215   - @Transactional
216 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 249 private Device doSaveDevice(Device device, String accessToken, boolean doValidate) {
... ... @@ -270,15 +282,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
270 282 device.setDeviceData(syncDeviceData(deviceProfile, device.getDeviceData()));
271 283 return deviceDao.save(device.getTenantId(), device);
272 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 368 }
365 369
366 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 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 384 @Override
... ... @@ -560,84 +565,93 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
560 565 }, MoreExecutors.directExecutor());
561 566 }
562 567
563   - @Transactional
564 568 @Override
565 569 public Device assignDeviceToTenant(TenantId tenantId, Device device) {
566 570 log.trace("Executing assignDeviceToTenant [{}][{}]", tenantId, device);
567   -
568 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 605 @Override
597 606 @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#profile.tenantId, #provisionRequest.deviceName}")
598   - @Transactional
599 607 public Device saveDevice(ProvisionRequest provisionRequest, DeviceProfile profile) {
600 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 657 @Override
... ... @@ -818,4 +832,20 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
818 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 +}
... ...