Handle Device service transactional methods exceptions (fix exception handling for devices with same name)

This commit is contained in:
Igor Kulikov 2021-09-02 15:54:17 +03:00
parent 0d7adb73eb
commit b67f454ba0
2 changed files with 168 additions and 102 deletions

View File

@ -30,7 +30,6 @@ import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching; import org.springframework.cache.annotation.Caching;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.thingsboard.common.util.JacksonUtil; 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.service.PaginatedRemover;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantDao; import org.thingsboard.server.dao.tenant.TenantDao;
import org.thingsboard.server.dao.tx.TransactionHandler;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
@ -141,6 +141,9 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
@Autowired @Autowired
private OtaPackageService otaPackageService; private OtaPackageService otaPackageService;
@Autowired
private TransactionHandler transactionHandler;
@Override @Override
public DeviceInfo findDeviceInfoById(TenantId tenantId, DeviceId deviceId) { public DeviceInfo findDeviceInfoById(TenantId tenantId, DeviceId deviceId) {
log.trace("Executing findDeviceInfoById [{}]", 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.name}"),
@CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.id}") @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.id}")
}) })
@Transactional
@Override @Override
public Device saveDeviceWithAccessToken(Device device, String accessToken) { 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= { @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.name}"),
@CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.id}") @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.id}")
}) })
@Transactional
@Override @Override
public Device saveDeviceWithCredentials(Device device, DeviceCredentials deviceCredentials) { public Device saveDeviceWithCredentials(Device toSave, DeviceCredentials deviceCredentials) {
if (device.getId() == null) { try {
Device deviceWithName = this.findDeviceByTenantIdAndName(device.getTenantId(), device.getName()); return transactionHandler.runInTransaction(() -> {
device = deviceWithName == null ? device : deviceWithName.updateDevice(device); 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) { 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())); device.setDeviceData(syncDeviceData(deviceProfile, device.getDeviceData()));
return deviceDao.save(device.getTenantId(), device); return deviceDao.save(device.getTenantId(), device);
} catch (Exception t) { } catch (Exception t) {
ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); throw handleDeviceSaveException(device, t);
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;
}
} }
} }
@ -364,16 +368,17 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
} }
private void removeDeviceFromCacheByName(TenantId tenantId, String name) { private void removeDeviceFromCacheByName(TenantId tenantId, String name) {
Cache cache = cacheManager.getCache(DEVICE_CACHE); if (tenantId != null && !StringUtils.isEmpty(name)) {
cache.evict(Arrays.asList(tenantId, name)); Cache cache = cacheManager.getCache(DEVICE_CACHE);
cache.evict(Arrays.asList(tenantId, name));
}
} }
private void removeDeviceFromCacheById(TenantId tenantId, DeviceId deviceId) { private void removeDeviceFromCacheById(TenantId tenantId, DeviceId deviceId) {
if (deviceId == null) { if (tenantId != null && deviceId != null) {
return; 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 @Override
@ -560,84 +565,93 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
}, MoreExecutors.directExecutor()); }, MoreExecutors.directExecutor());
} }
@Transactional
@Override @Override
public Device assignDeviceToTenant(TenantId tenantId, Device device) { public Device assignDeviceToTenant(TenantId tenantId, Device device) {
log.trace("Executing assignDeviceToTenant [{}][{}]", tenantId, device); log.trace("Executing assignDeviceToTenant [{}][{}]", tenantId, device);
try { try {
List<EntityView> entityViews = entityViewService.findEntityViewsByTenantIdAndEntityIdAsync(device.getTenantId(), device.getId()).get(); return transactionHandler.runInTransaction(() -> {
if (!CollectionUtils.isEmpty(entityViews)) { try {
throw new DataValidationException("Can't assign device that has entity views to another tenant!"); List<EntityView> entityViews = entityViewService.findEntityViewsByTenantIdAndEntityIdAsync(device.getTenantId(), device.getId()).get();
} if (!CollectionUtils.isEmpty(entityViews)) {
} catch (ExecutionException | InterruptedException e) { throw new DataValidationException("Can't assign device that has entity views to another tenant!");
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); } 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 @Override
@CacheEvict(cacheNames = DEVICE_CACHE, key = "{#profile.tenantId, #provisionRequest.deviceName}") @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#profile.tenantId, #provisionRequest.deviceName}")
@Transactional
public Device saveDevice(ProvisionRequest provisionRequest, DeviceProfile profile) { public Device saveDevice(ProvisionRequest provisionRequest, DeviceProfile profile) {
Device device = new Device(); Device device = new Device();
device.setName(provisionRequest.getDeviceName()); try {
device.setType(profile.getName()); return transactionHandler.runInTransaction(() -> {
device.setTenantId(profile.getTenantId()); device.setName(provisionRequest.getDeviceName());
Device savedDevice = saveDevice(device); device.setType(profile.getName());
if (!StringUtils.isEmpty(provisionRequest.getCredentialsData().getToken()) || device.setTenantId(profile.getTenantId());
!StringUtils.isEmpty(provisionRequest.getCredentialsData().getX509CertHash()) || Device savedDevice = saveDevice(device);
!StringUtils.isEmpty(provisionRequest.getCredentialsData().getUsername()) || if (!StringUtils.isEmpty(provisionRequest.getCredentialsData().getToken()) ||
!StringUtils.isEmpty(provisionRequest.getCredentialsData().getPassword()) || !StringUtils.isEmpty(provisionRequest.getCredentialsData().getX509CertHash()) ||
!StringUtils.isEmpty(provisionRequest.getCredentialsData().getClientId())) { !StringUtils.isEmpty(provisionRequest.getCredentialsData().getUsername()) ||
DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedDevice.getTenantId(), savedDevice.getId()); !StringUtils.isEmpty(provisionRequest.getCredentialsData().getPassword()) ||
if (deviceCredentials == null) { !StringUtils.isEmpty(provisionRequest.getCredentialsData().getClientId())) {
deviceCredentials = new DeviceCredentials(); DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedDevice.getTenantId(), savedDevice.getId());
} if (deviceCredentials == null) {
deviceCredentials.setDeviceId(savedDevice.getId()); deviceCredentials = new DeviceCredentials();
deviceCredentials.setCredentialsType(provisionRequest.getCredentialsType()); }
switch (provisionRequest.getCredentialsType()) { deviceCredentials.setDeviceId(savedDevice.getId());
case ACCESS_TOKEN: deviceCredentials.setCredentialsType(provisionRequest.getCredentialsType());
deviceCredentials.setCredentialsId(provisionRequest.getCredentialsData().getToken()); switch (provisionRequest.getCredentialsType()) {
break; case ACCESS_TOKEN:
case MQTT_BASIC: deviceCredentials.setCredentialsId(provisionRequest.getCredentialsData().getToken());
BasicMqttCredentials mqttCredentials = new BasicMqttCredentials(); break;
mqttCredentials.setClientId(provisionRequest.getCredentialsData().getClientId()); case MQTT_BASIC:
mqttCredentials.setUserName(provisionRequest.getCredentialsData().getUsername()); BasicMqttCredentials mqttCredentials = new BasicMqttCredentials();
mqttCredentials.setPassword(provisionRequest.getCredentialsData().getPassword()); mqttCredentials.setClientId(provisionRequest.getCredentialsData().getClientId());
deviceCredentials.setCredentialsValue(JacksonUtil.toString(mqttCredentials)); mqttCredentials.setUserName(provisionRequest.getCredentialsData().getUsername());
break; mqttCredentials.setPassword(provisionRequest.getCredentialsData().getPassword());
case X509_CERTIFICATE: deviceCredentials.setCredentialsValue(JacksonUtil.toString(mqttCredentials));
deviceCredentials.setCredentialsValue(provisionRequest.getCredentialsData().getX509CertHash()); break;
break; case X509_CERTIFICATE:
case LWM2M_CREDENTIALS: deviceCredentials.setCredentialsValue(provisionRequest.getCredentialsData().getX509CertHash());
break; break;
} case LWM2M_CREDENTIALS:
try { break;
deviceCredentialsService.updateDeviceCredentials(savedDevice.getTenantId(), deviceCredentials); }
} catch (Exception e) { try {
throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name()); 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 @Override
@ -818,4 +832,20 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
unassignDeviceFromCustomer(tenantId, new DeviceId(entity.getUuidId())); 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);
}
}
} }

View File

@ -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> T runInTransaction(Supplier<T> supplier) {
return supplier.get();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public <T> T runInNewTransaction(Supplier<T> supplier) {
return supplier.get();
}
}