diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java index 39b5eb7eac..3b18dc24cd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java @@ -30,7 +30,6 @@ import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Caching; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import org.thingsboard.common.util.JacksonUtil; @@ -82,6 +81,7 @@ import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantDao; +import org.thingsboard.server.dao.tx.TransactionHandler; import javax.annotation.Nullable; import java.util.ArrayList; @@ -141,6 +141,9 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe @Autowired private OtaPackageService otaPackageService; + @Autowired + private TransactionHandler transactionHandler; + @Override public DeviceInfo findDeviceInfoById(TenantId tenantId, DeviceId deviceId) { log.trace("Executing findDeviceInfoById [{}]", deviceId); @@ -184,10 +187,13 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.name}"), @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.id}") }) - @Transactional @Override public Device saveDeviceWithAccessToken(Device device, String accessToken) { - return doSaveDevice(device, accessToken, true); + try { + return transactionHandler.runInTransaction(() -> doSaveDevice(device, accessToken, true)); + } catch (Exception t) { + throw handleDeviceSaveException(device, t); + } } @Caching(evict= { @@ -212,26 +218,32 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.name}"), @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.id}") }) - @Transactional @Override - public Device saveDeviceWithCredentials(Device device, DeviceCredentials deviceCredentials) { - if (device.getId() == null) { - Device deviceWithName = this.findDeviceByTenantIdAndName(device.getTenantId(), device.getName()); - device = deviceWithName == null ? device : deviceWithName.updateDevice(device); + public Device saveDeviceWithCredentials(Device toSave, DeviceCredentials deviceCredentials) { + try { + return transactionHandler.runInTransaction(() -> { + Device device = toSave; + if (device.getId() == null) { + Device deviceWithName = this.findDeviceByTenantIdAndName(device.getTenantId(), device.getName()); + device = deviceWithName == null ? device : deviceWithName.updateDevice(device); + } + Device savedDevice = this.saveDeviceWithoutCredentials(device, true); + deviceCredentials.setDeviceId(savedDevice.getId()); + if (device.getId() == null) { + deviceCredentialsService.createDeviceCredentials(savedDevice.getTenantId(), deviceCredentials); + } else { + DeviceCredentials foundDeviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getTenantId(), savedDevice.getId()); + if (foundDeviceCredentials == null) { + deviceCredentialsService.createDeviceCredentials(savedDevice.getTenantId(), deviceCredentials); + } else { + deviceCredentialsService.updateDeviceCredentials(device.getTenantId(), deviceCredentials); + } + } + return savedDevice; + }); + } catch (Exception t) { + throw handleDeviceSaveException(toSave, t); } - Device savedDevice = this.saveDeviceWithoutCredentials(device, true); - deviceCredentials.setDeviceId(savedDevice.getId()); - if (device.getId() == null) { - deviceCredentialsService.createDeviceCredentials(savedDevice.getTenantId(), deviceCredentials); - } else { - DeviceCredentials foundDeviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getTenantId(), savedDevice.getId()); - if (foundDeviceCredentials == null) { - deviceCredentialsService.createDeviceCredentials(savedDevice.getTenantId(), deviceCredentials); - } else { - deviceCredentialsService.updateDeviceCredentials(device.getTenantId(), deviceCredentials); - } - } - return savedDevice; } private Device doSaveDevice(Device device, String accessToken, boolean doValidate) { @@ -270,15 +282,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe device.setDeviceData(syncDeviceData(deviceProfile, device.getDeviceData())); return deviceDao.save(device.getTenantId(), device); } catch (Exception t) { - ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); - if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("device_name_unq_key")) { - // remove device from cache in case null value cached in the distributed redis. - removeDeviceFromCacheByName(device.getTenantId(), device.getName()); - removeDeviceFromCacheById(device.getTenantId(), device.getId()); - throw new DataValidationException("Device with such name already exists!"); - } else { - throw t; - } + throw handleDeviceSaveException(device, t); } } @@ -364,16 +368,17 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe } private void removeDeviceFromCacheByName(TenantId tenantId, String name) { - Cache cache = cacheManager.getCache(DEVICE_CACHE); - cache.evict(Arrays.asList(tenantId, name)); + if (tenantId != null && !StringUtils.isEmpty(name)) { + Cache cache = cacheManager.getCache(DEVICE_CACHE); + cache.evict(Arrays.asList(tenantId, name)); + } } private void removeDeviceFromCacheById(TenantId tenantId, DeviceId deviceId) { - if (deviceId == null) { - return; + if (tenantId != null && deviceId != null) { + Cache cache = cacheManager.getCache(DEVICE_CACHE); + cache.evict(Arrays.asList(tenantId, deviceId)); } - Cache cache = cacheManager.getCache(DEVICE_CACHE); - cache.evict(Arrays.asList(tenantId, deviceId)); } @Override @@ -560,84 +565,93 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe }, MoreExecutors.directExecutor()); } - @Transactional @Override public Device assignDeviceToTenant(TenantId tenantId, Device device) { log.trace("Executing assignDeviceToTenant [{}][{}]", tenantId, device); - try { - List entityViews = entityViewService.findEntityViewsByTenantIdAndEntityIdAsync(device.getTenantId(), device.getId()).get(); - if (!CollectionUtils.isEmpty(entityViews)) { - throw new DataValidationException("Can't assign device that has entity views to another tenant!"); - } - } catch (ExecutionException | InterruptedException e) { - log.error("Exception while finding entity views for deviceId [{}]", device.getId(), e); - throw new RuntimeException("Exception while finding entity views for deviceId [" + device.getId() + "]", e); + return transactionHandler.runInTransaction(() -> { + try { + List entityViews = entityViewService.findEntityViewsByTenantIdAndEntityIdAsync(device.getTenantId(), device.getId()).get(); + if (!CollectionUtils.isEmpty(entityViews)) { + throw new DataValidationException("Can't assign device that has entity views to another tenant!"); + } + } catch (ExecutionException | InterruptedException e) { + log.error("Exception while finding entity views for deviceId [{}]", device.getId(), e); + throw new RuntimeException("Exception while finding entity views for deviceId [" + device.getId() + "]", e); + } + + eventService.removeEvents(device.getTenantId(), device.getId()); + + relationService.removeRelations(device.getTenantId(), device.getId()); + + TenantId oldTenantId = device.getTenantId(); + + device.setTenantId(tenantId); + device.setCustomerId(null); + Device savedDevice = doSaveDevice(device, null, true); + + // explicitly remove device with previous tenant id from cache + // result device object will have different tenant id and will not remove entity from cache + removeDeviceFromCacheByName(oldTenantId, device.getName()); + removeDeviceFromCacheById(oldTenantId, device.getId()); + + return savedDevice; + }); + } catch (Exception t) { + throw handleDeviceSaveException(device, t); } - - eventService.removeEvents(device.getTenantId(), device.getId()); - - relationService.removeRelations(device.getTenantId(), device.getId()); - - TenantId oldTenantId = device.getTenantId(); - - device.setTenantId(tenantId); - device.setCustomerId(null); - Device savedDevice = doSaveDevice(device, null, true); - - // explicitly remove device with previous tenant id from cache - // result device object will have different tenant id and will not remove entity from cache - removeDeviceFromCacheByName(oldTenantId, device.getName()); - removeDeviceFromCacheById(oldTenantId, device.getId()); - - return savedDevice; } @Override @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#profile.tenantId, #provisionRequest.deviceName}") - @Transactional public Device saveDevice(ProvisionRequest provisionRequest, DeviceProfile profile) { Device device = new Device(); - device.setName(provisionRequest.getDeviceName()); - device.setType(profile.getName()); - device.setTenantId(profile.getTenantId()); - Device savedDevice = saveDevice(device); - if (!StringUtils.isEmpty(provisionRequest.getCredentialsData().getToken()) || - !StringUtils.isEmpty(provisionRequest.getCredentialsData().getX509CertHash()) || - !StringUtils.isEmpty(provisionRequest.getCredentialsData().getUsername()) || - !StringUtils.isEmpty(provisionRequest.getCredentialsData().getPassword()) || - !StringUtils.isEmpty(provisionRequest.getCredentialsData().getClientId())) { - DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedDevice.getTenantId(), savedDevice.getId()); - if (deviceCredentials == null) { - deviceCredentials = new DeviceCredentials(); - } - deviceCredentials.setDeviceId(savedDevice.getId()); - deviceCredentials.setCredentialsType(provisionRequest.getCredentialsType()); - switch (provisionRequest.getCredentialsType()) { - case ACCESS_TOKEN: - deviceCredentials.setCredentialsId(provisionRequest.getCredentialsData().getToken()); - break; - case MQTT_BASIC: - BasicMqttCredentials mqttCredentials = new BasicMqttCredentials(); - mqttCredentials.setClientId(provisionRequest.getCredentialsData().getClientId()); - mqttCredentials.setUserName(provisionRequest.getCredentialsData().getUsername()); - mqttCredentials.setPassword(provisionRequest.getCredentialsData().getPassword()); - deviceCredentials.setCredentialsValue(JacksonUtil.toString(mqttCredentials)); - break; - case X509_CERTIFICATE: - deviceCredentials.setCredentialsValue(provisionRequest.getCredentialsData().getX509CertHash()); - break; - case LWM2M_CREDENTIALS: - break; - } - try { - deviceCredentialsService.updateDeviceCredentials(savedDevice.getTenantId(), deviceCredentials); - } catch (Exception e) { - throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name()); - } + try { + return transactionHandler.runInTransaction(() -> { + device.setName(provisionRequest.getDeviceName()); + device.setType(profile.getName()); + device.setTenantId(profile.getTenantId()); + Device savedDevice = saveDevice(device); + if (!StringUtils.isEmpty(provisionRequest.getCredentialsData().getToken()) || + !StringUtils.isEmpty(provisionRequest.getCredentialsData().getX509CertHash()) || + !StringUtils.isEmpty(provisionRequest.getCredentialsData().getUsername()) || + !StringUtils.isEmpty(provisionRequest.getCredentialsData().getPassword()) || + !StringUtils.isEmpty(provisionRequest.getCredentialsData().getClientId())) { + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedDevice.getTenantId(), savedDevice.getId()); + if (deviceCredentials == null) { + deviceCredentials = new DeviceCredentials(); + } + deviceCredentials.setDeviceId(savedDevice.getId()); + deviceCredentials.setCredentialsType(provisionRequest.getCredentialsType()); + switch (provisionRequest.getCredentialsType()) { + case ACCESS_TOKEN: + deviceCredentials.setCredentialsId(provisionRequest.getCredentialsData().getToken()); + break; + case MQTT_BASIC: + BasicMqttCredentials mqttCredentials = new BasicMqttCredentials(); + mqttCredentials.setClientId(provisionRequest.getCredentialsData().getClientId()); + mqttCredentials.setUserName(provisionRequest.getCredentialsData().getUsername()); + mqttCredentials.setPassword(provisionRequest.getCredentialsData().getPassword()); + deviceCredentials.setCredentialsValue(JacksonUtil.toString(mqttCredentials)); + break; + case X509_CERTIFICATE: + deviceCredentials.setCredentialsValue(provisionRequest.getCredentialsData().getX509CertHash()); + break; + case LWM2M_CREDENTIALS: + break; + } + try { + deviceCredentialsService.updateDeviceCredentials(savedDevice.getTenantId(), deviceCredentials); + } catch (Exception e) { + throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name()); + } + } + removeDeviceFromCacheById(savedDevice.getTenantId(), savedDevice.getId()); // eviction by name is described as annotation @CacheEvict above + return savedDevice; + }); + } catch (Exception t) { + throw handleDeviceSaveException(device, t); } - removeDeviceFromCacheById(savedDevice.getTenantId(), savedDevice.getId()); // eviction by name is described as annotation @CacheEvict above - return savedDevice; } @Override @@ -818,4 +832,20 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe unassignDeviceFromCustomer(tenantId, new DeviceId(entity.getUuidId())); } }; + + private RuntimeException handleDeviceSaveException(Device device, Exception t) { + ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); + if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("device_name_unq_key")) { + // remove device from cache in case null value cached in the distributed redis. + if (device != null) { + removeDeviceFromCacheByName(device.getTenantId(), device.getName()); + removeDeviceFromCacheById(device.getTenantId(), device.getId()); + } + return new DataValidationException("Device with such name already exists!"); + } else if (t instanceof RuntimeException) { + return (RuntimeException)t; + } else { + return new RuntimeException("Failed to save device!", t); + } + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/tx/TransactionHandler.java b/dao/src/main/java/org/thingsboard/server/dao/tx/TransactionHandler.java new file mode 100644 index 0000000000..3b538f8187 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/tx/TransactionHandler.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2021 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.tx; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import java.util.function.Supplier; + +@Service +public class TransactionHandler { + + @Transactional(propagation = Propagation.REQUIRED) + public T runInTransaction(Supplier supplier) { + return supplier.get(); + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public T runInNewTransaction(Supplier supplier) { + return supplier.get(); + } +}