From 77c74d7ccd70db15089dbbdf56c2d31ced76b54c Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 16 Jun 2026 12:43:33 +0300 Subject: [PATCH] fix(iot-hub): compensate orphaned entities when installed-item tracking save fails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If iotHubInstalledItemService.save throws after the local entity was already created, the entity was orphaned — visible in the tenant but invisible to IoT Hub update/delete. Extracted the per-type entity-deletion dispatch from deleteInstalledItem into a deleteEntityForDescriptor helper and call it from doInstallVersion and registerDeviceInstall on tracking-save failure. registerDeviceInstall now also wraps reportVersionInstalled best-effort to match doInstallVersion. --- .../service/iot_hub/DefaultIotHubService.java | 56 ++++++++++++++++--- 1 file changed, 49 insertions(+), 7 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java b/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java index fbd7e4160b..e88d812f5b 100644 --- a/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java +++ b/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java @@ -178,7 +178,22 @@ public class DefaultIotHubService implements IotHubService { installedItem.setItemType(itemType); installedItem.setVersion(version); installedItem.setDescriptor(descriptor); - installedItem = iotHubInstalledItemService.save(tenantId, installedItem); + try { + installedItem = iotHubInstalledItemService.save(tenantId, installedItem); + } catch (Exception e) { + // Tracking save failed after the entity was already created locally — compensate by + // deleting the orphaned entity so the user is not left with an installed item that + // IoT Hub update/delete flows cannot see. + log.error("[{}] Failed to save IoT Hub installed-item tracking row for {} ({}); rolling back created entity", + tenantId, itemName, itemType, e); + try { + deleteEntityForDescriptor(user, tenantId, descriptor, null); + } catch (Exception rb) { + log.error("[{}] Compensating delete after failed tracking save threw — entity may remain orphaned", + tenantId, rb); + } + throw e; + } try { iotHubRestClient.reportVersionInstalled(versionId); @@ -620,9 +635,26 @@ public class DefaultIotHubService implements IotHubService { installedItem.setItemType("DEVICE"); installedItem.setVersion(version); installedItem.setDescriptor(descriptor); - iotHubInstalledItemService.save(tenantId, installedItem); + try { + iotHubInstalledItemService.save(tenantId, installedItem); + } catch (Exception e) { + log.error("[{}] Failed to save IoT Hub installed-item tracking row for device package {} (version {}); rolling back created entities", + tenantId, itemName, version, e); + try { + deleteEntityForDescriptor(user, tenantId, descriptor, null); + } catch (Exception rb) { + log.error("[{}] Compensating delete after failed tracking save threw — device package entities may remain orphaned", + tenantId, rb); + } + throw e; + } - iotHubRestClient.reportVersionInstalled(versionId); + try { + iotHubRestClient.reportVersionInstalled(versionId); + } catch (Exception e) { + // Counter ping is best-effort — do not fail the install if it errors. + log.warn("[{}] Failed to report install counter for version {}: {}", tenantId, versionId, e.getMessage()); + } log.info("[{}] Registered device package install: {} (version {})", tenantId, itemName, version); return InstallItemVersionResult.success(descriptor); @@ -677,7 +709,20 @@ public class DefaultIotHubService implements IotHubService { throw new IllegalArgumentException("Installed item not found"); } - IotHubInstalledItemDescriptor descriptor = installedItem.getDescriptor(); + deleteEntityForDescriptor(user, tenantId, installedItem.getDescriptor(), installedItemId); + + iotHubInstalledItemService.deleteById(tenantId, installedItemId); + log.info("[{}] Deleted installed IoT Hub item: {}", tenantId, installedItem.getItemName()); + } + + /** + * Per-type dispatcher that removes the local entities a marketplace install created. Shared by + * {@link #deleteInstalledItem} and by the tracking-row compensation path in + * {@link #doInstallVersion} so a failed tracking save does not leave the entity orphaned. + */ + private void deleteEntityForDescriptor(SecurityUser user, TenantId tenantId, + IotHubInstalledItemDescriptor descriptor, + IotHubInstalledItemId installedItemId) { if (descriptor instanceof WidgetInstalledItemDescriptor wd) { WidgetTypeDetails widgetType = widgetTypeService.findWidgetTypeDetailsById(tenantId, wd.getWidgetTypeId()); if (widgetType != null) { @@ -709,9 +754,6 @@ public class DefaultIotHubService implements IotHubService { log.error("[{}] Failed to delete solution for installed item {}", tenantId, installedItemId, e); } } - - iotHubInstalledItemService.deleteById(tenantId, installedItemId); - log.info("[{}] Deleted installed IoT Hub item: {}", tenantId, installedItem.getItemName()); } @Override