Commit b67f454ba04110e338e23cd37e2a438d889d7c78
1 parent
0d7adb73
Handle Device service transactional methods exceptions (fix exception handling f…
…or devices with same name)
Showing
2 changed files
with
163 additions
and
97 deletions
... | ... | @@ -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 | +} | ... | ... |