From cfd15fbf1058ceec212ba289c835c08c0725b08e Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Wed, 2 Sep 2026 13:07:58 +0000 Subject: [PATCH 1/5] Move the IoT hub in the test template off a retired preview API version Every pipeline has been failing in deployCloudTestResources since 2026-09-01, on main as well as on pull requests, before any test runs: ERROR: {"status":"Failed","error":{"code":"DeploymentFailed", "details":[{"code":"BadRequest", ... e2eTestsSetup.ps1:89 throw "Error running resource group deployment." Nothing under vsts/E2ETestsSetup had changed since May, so the trigger is service side. The template pinned Microsoft.Devices/IotHubs to 2021-03-03-preview; preview API versions are retired on a much shorter horizon than GA ones, and a retired version fails exactly this way. Every other resource in the template is already on a GA version. Moved to 2023-06-30, the newest GA version. The properties the template sets - eventHubEndpoints, cloudToDevice, messagingEndpoints, StorageEndpoints, enableFileUploadNotifications, sku - are all GA stable and unchanged. test-resources.json is regenerated from the bicep, so it also picks up EnableIotHubSecuritySolution, a parameter the bicep has declared since January that the committed JSON was missing. It is never referenced, and it defaults to false, so it changes nothing at deployment. Resource list and outputs are otherwise identical. --- vsts/E2ETestsSetup/test-resources.bicep | 2 +- vsts/E2ETestsSetup/test-resources.json | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/vsts/E2ETestsSetup/test-resources.bicep b/vsts/E2ETestsSetup/test-resources.bicep index 76b4bb0227..1fb7e07686 100644 --- a/vsts/E2ETestsSetup/test-resources.bicep +++ b/vsts/E2ETestsSetup/test-resources.bicep @@ -70,7 +70,7 @@ resource container 'Microsoft.Storage/storageAccounts/blobServices/containers@20 } } -resource iotHub 'Microsoft.Devices/IotHubs@2021-03-03-preview' = { +resource iotHub 'Microsoft.Devices/IotHubs@2023-06-30' = { name: HubName location: resourceGroup().location identity: { diff --git a/vsts/E2ETestsSetup/test-resources.json b/vsts/E2ETestsSetup/test-resources.json index 976218a9dc..73c39cd852 100644 --- a/vsts/E2ETestsSetup/test-resources.json +++ b/vsts/E2ETestsSetup/test-resources.json @@ -4,18 +4,18 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.15.31.15270", - "templateHash": "3831266858947033411" + "version": "0.46.1.21595", + "templateHash": "7187049160758863132" } }, "parameters": { "StorageAccountName": { "type": "string", + "minLength": 3, + "maxLength": 24, "metadata": { "description": "The name of the storage account used by the IoT hub." - }, - "maxLength": 24, - "minLength": 3 + } }, "HubName": { "type": "string", @@ -58,6 +58,13 @@ "metadata": { "description": "The name of the Container inside the BlobService." } + }, + "EnableIotHubSecuritySolution": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Flag to indicate if IoT hub should have security solution enabled." + } } }, "variables": { @@ -118,7 +125,7 @@ }, { "type": "Microsoft.Devices/IotHubs", - "apiVersion": "2021-03-03-preview", + "apiVersion": "2023-06-30", "name": "[parameters('HubName')]", "location": "[resourceGroup().location]", "identity": { From 81c8157b2b08760605eedad6c603e4ed503c0501 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Wed, 2 Sep 2026 13:15:17 +0000 Subject: [PATCH 2/5] Use the documented storageEndpoints casing on the IoT hub The template wrote StorageEndpoints. ARM matches property names case insensitively, which is why file upload has been configured correctly all along, but the documented schema is storageEndpoints and the bicep type checker is case sensitive, so the wrong casing produced a BCP089 warning saying the property is not allowed. Emitted template is byte identical apart from that one key, so this changes no deployed configuration. It clears both BCP089 warnings, and it means the template no longer relies on case insensitive matching. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- vsts/E2ETestsSetup/test-resources.bicep | 2 +- vsts/E2ETestsSetup/test-resources.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/vsts/E2ETestsSetup/test-resources.bicep b/vsts/E2ETestsSetup/test-resources.bicep index 1fb7e07686..8fd20edb25 100644 --- a/vsts/E2ETestsSetup/test-resources.bicep +++ b/vsts/E2ETestsSetup/test-resources.bicep @@ -99,7 +99,7 @@ resource iotHub 'Microsoft.Devices/IotHubs@2023-06-30' = { maxDeliveryCount: 100 } } - StorageEndpoints: { + storageEndpoints: { '$default': { sasTtlAsIso8601: 'PT1H' connectionString: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};AccountKey=${listkeys(storageAccount.id, '2019-06-01').keys[0].value}' diff --git a/vsts/E2ETestsSetup/test-resources.json b/vsts/E2ETestsSetup/test-resources.json index 73c39cd852..b2eae1c46f 100644 --- a/vsts/E2ETestsSetup/test-resources.json +++ b/vsts/E2ETestsSetup/test-resources.json @@ -154,7 +154,7 @@ "maxDeliveryCount": 100 } }, - "StorageEndpoints": { + "storageEndpoints": { "$default": { "sasTtlAsIso8601": "PT1H", "connectionString": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}', parameters('StorageAccountName'), listkeys(resourceId('Microsoft.Storage/storageAccounts', parameters('StorageAccountName')), '2019-06-01').keys[0].value)]", From e1b5127fd9a65f4ae475d25db5c0e7142ddd17a2 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Sat, 5 Sep 2026 03:26:36 +0000 Subject: [PATCH 3/5] Drop the unused security solution parameter from the test template EnableIotHubSecuritySolution has been declared in the bicep since January and is referenced nowhere: not by another parameter, not by a resource, not by an output, and not by e2eTestsSetup.ps1, which passes only StorageAccountName and HubUnitsCount. No security solution resource has ever existed in this template, so it was never wired to anything. It stayed invisible because the committed JSON had drifted from the bicep and did not carry it. Regenerating the JSON in this change would have published a dead parameter to callers, so it is removed at the source instead. The emitted template now has the same seven parameters as before, so this removes the only parameter that regenerating would otherwise have added. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- vsts/E2ETestsSetup/test-resources.bicep | 3 --- vsts/E2ETestsSetup/test-resources.json | 9 +-------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/vsts/E2ETestsSetup/test-resources.bicep b/vsts/E2ETestsSetup/test-resources.bicep index 8fd20edb25..99a7043d11 100644 --- a/vsts/E2ETestsSetup/test-resources.bicep +++ b/vsts/E2ETestsSetup/test-resources.bicep @@ -21,9 +21,6 @@ param BlobServiceName string = 'default' @description('The name of the Container inside the BlobService.') param ContainerName string = 'fileupload' -@description('Flag to indicate if IoT hub should have security solution enabled.') -param EnableIotHubSecuritySolution bool = false - var hubKeysId = resourceId('Microsoft.Devices/IotHubs/Iothubkeys', HubName, 'iothubowner') var dpsKeysId = resourceId('Microsoft.Devices/ProvisioningServices/keys', DpsName, 'provisioningserviceowner') diff --git a/vsts/E2ETestsSetup/test-resources.json b/vsts/E2ETestsSetup/test-resources.json index b2eae1c46f..ceee2a88a3 100644 --- a/vsts/E2ETestsSetup/test-resources.json +++ b/vsts/E2ETestsSetup/test-resources.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "0.46.1.21595", - "templateHash": "7187049160758863132" + "templateHash": "3528572825137063880" } }, "parameters": { @@ -58,13 +58,6 @@ "metadata": { "description": "The name of the Container inside the BlobService." } - }, - "EnableIotHubSecuritySolution": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Flag to indicate if IoT hub should have security solution enabled." - } } }, "variables": { From dc3f070e14df64ae5e64e30d4726761b166780be Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Sat, 5 Sep 2026 06:06:29 +0000 Subject: [PATCH 4/5] Scope the test instance lifecycle to one attempt, and log around the open RerunFailedTestRule reuses the same test instance for every attempt at a test, so "teardown has run" could not be a plain flag on that instance. It was. Once the first attempt's @After had run, every later attempt called setup(), published its fresh client, and disposeIfTeardownAlreadyRan immediately closed it. open() then failed with "Client was closed while attempting to open the connection", turning one flaky timeout into a guaranteed failure of all three attempts. Build 163484 shows it: run 3 acquired its device at 03:39:27.203 and logged "Closing device client" at 03:39:27.768, half a second later and before any connect. Each setup now takes a generation number and teardown records the generation it covered, so a setup only cleans up after itself when teardown has run for its own generation. beginSetup also reclaims any residue from a previous attempt first, which is normally nothing. The same generation check makes a superseded setup safe: a setup abandoned by the timeout that finishes after a later attempt has started now reclaims what it produced instead of publishing it over the identity the running attempt is using. Also logs around identity acquisition and around open. The stalls seen so far go silent after "Acquiring test device", which does not distinguish a setup stuck getting an identity from a connect that never completes. These three lines separate the two. Verified the lifecycle across the interleavings: three reruns, teardown mid setup, the ECC variant of that, a superseded setup overlapping a later attempt, normal ECC ordering, and repeated dispose. The rerun cases fail against the previous model and pass against this one. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../iothub/connection/ConnectionTests.java | 210 ++++++++++++++---- 1 file changed, 171 insertions(+), 39 deletions(-) diff --git a/iot-e2e-tests/common/src/test/java/tests/integration/com/microsoft/azure/sdk/iot/iothub/connection/ConnectionTests.java b/iot-e2e-tests/common/src/test/java/tests/integration/com/microsoft/azure/sdk/iot/iothub/connection/ConnectionTests.java index 11e69f6909..f2b50ae30f 100644 --- a/iot-e2e-tests/common/src/test/java/tests/integration/com/microsoft/azure/sdk/iot/iothub/connection/ConnectionTests.java +++ b/iot-e2e-tests/common/src/test/java/tests/integration/com/microsoft/azure/sdk/iot/iothub/connection/ConnectionTests.java @@ -113,14 +113,26 @@ public class ConnectionTestInstance // same client twice and, worse, requeue the same identity into the shared pool twice. private TestIdentity identityToDispose; - // Set once setupEccDevice starts creating an identity, and never cleared. Identity classification has to - // outlive eccDeviceIdToDelete: teardown claims and clears that id, so if teardown runs between the device - // being registered and the identity being published, the late disposeIfTeardownAlreadyRan would see an - // identity with no ecc id and hand a self signed identity to the shared x509 pool. - private volatile boolean identityIsEcc; - - // Set once teardown has run. Guarded by lifecycleLock along with the two fields above. - private boolean disposed; + // Set once setupEccDevice starts creating an identity, and cleared only when a new attempt begins. + // Identity classification has to outlive eccDeviceIdToDelete: teardown claims and clears that id, so if + // teardown runs between the device being registered and the identity being published, the late + // disposeIfTeardownAlreadyRan would see an identity with no ecc id and hand a self signed identity to the + // shared x509 pool. + private boolean identityIsEcc; + + // Which setup attempt this instance is currently serving, and how far teardown has run. + // + // RerunFailedTestRule reuses this instance for every attempt at a test, so lifecycle state cannot be a plain + // "teardown has happened" flag. It was, and the result was that once the first attempt's @After had run, + // every later attempt disposed its own freshly acquired client the moment setup published it, and open() + // then failed with "Client was closed while attempting to open the connection". One flaky timeout became a + // guaranteed failure of every remaining attempt. + // + // Each setup takes a generation number, and teardown records the generation it covered. A setup only cleans + // up after itself if teardown has already run for its own generation, so an earlier attempt's teardown can + // no longer reach into a later attempt's. + private long setupGeneration; + private long disposedThrough; private final Object lifecycleLock = new Object(); public AuthenticationType authenticationType; @@ -166,26 +178,37 @@ private void applyProxySettings(ClientOptions.ClientOptionsBuilder optionsBuilde public void setup() throws Exception { + long generation = beginSetup(); + ClientOptions.ClientOptionsBuilder optionsBuilder = ClientOptions.builder(); applyProxySettings(optionsBuilder); + log.info("Acquiring test identity"); + if (clientType == ClientType.DEVICE_CLIENT) { - trackForCleanup(Tools.getTestDevice(iotHubConnectionString, this.protocol, this.authenticationType, false, optionsBuilder)); + trackForCleanup(generation, Tools.getTestDevice(iotHubConnectionString, this.protocol, this.authenticationType, false, optionsBuilder), false); } else if (clientType == ClientType.MODULE_CLIENT) { - trackForCleanup(Tools.getTestModule(iotHubConnectionString, this.protocol, this.authenticationType , false, optionsBuilder)); + trackForCleanup(generation, Tools.getTestModule(iotHubConnectionString, this.protocol, this.authenticationType , false, optionsBuilder), false); } - disposeIfTeardownAlreadyRan(); + log.info("Test identity acquired"); + + disposeIfTeardownAlreadyRan(generation); } public void setupEccDevice() throws Exception { + long generation = beginSetup(); + // Marked before anything is created, so that every identity this method goes on to publish is classified // as ECC even if teardown runs partway through. - this.identityIsEcc = true; + synchronized (lifecycleLock) + { + this.identityIsEcc = true; + } ClientOptions.ClientOptionsBuilder optionsBuilder = ClientOptions.builder(); applyProxySettings(optionsBuilder); @@ -200,12 +223,12 @@ public void setupEccDevice() throws Exception eccDevice.setThumbprint(certificateGenerator.getX509Thumbprint(), certificateGenerator.getX509Thumbprint()); Tools.addDeviceWithRetry(new RegistryClient(iotHubConnectionString), eccDevice); - trackEccDeviceForCleanup(eccDevice.getDeviceId()); + trackEccDeviceForCleanup(generation, eccDevice.getDeviceId()); String deviceConnectionString = Tools.getDeviceConnectionString(iotHubConnectionString, eccDevice); - trackForCleanup(new TestDeviceIdentity( + trackForCleanup(generation, new TestDeviceIdentity( new DeviceClient(deviceConnectionString, testInstance.protocol, optionsBuilder.build()), - eccDevice)); + eccDevice), true); } else if (clientType == ClientType.MODULE_CLIENT) { @@ -215,52 +238,147 @@ else if (clientType == ClientType.MODULE_CLIENT) eccModule.setThumbprint(certificateGenerator.getX509Thumbprint(), certificateGenerator.getX509Thumbprint()); Tools.addDeviceWithRetry(new RegistryClient(iotHubConnectionString), eccDevice); - trackEccDeviceForCleanup(eccDevice.getDeviceId()); + trackEccDeviceForCleanup(generation, eccDevice.getDeviceId()); Tools.addModuleWithRetry(new RegistryClient(iotHubConnectionString), eccModule); String moduleConnectionString = Tools.getDeviceConnectionString(iotHubConnectionString, eccDevice) + ";ModuleId=" + eccModule.getId(); - trackForCleanup(new TestModuleIdentity( + trackForCleanup(generation, new TestModuleIdentity( new ModuleClient(moduleConnectionString, testInstance.protocol, optionsBuilder.build()), eccDevice, - eccModule)); + eccModule), true); } - disposeIfTeardownAlreadyRan(); + disposeIfTeardownAlreadyRan(generation); + } + + /** + * Begin a new setup attempt and take its generation number. + * + *

Reclaims anything a previous attempt left behind first. Normally there is nothing: that attempt's + * {@code @After} already claimed and cleared what it owned. Anything still present is residue from a setup + * abandoned by the timeout, and disposing it here is the last chance to reclaim it.

+ * + * @return The generation number this setup attempt owns + */ + private long beginSetup() + { + dispose(); + + synchronized (lifecycleLock) + { + return ++this.setupGeneration; + } } /** * Hand an identity to teardown, and publish it for the test body to use. * + * @param generation The generation of the setup attempt that produced this identity * @param newIdentity The identity this test just acquired or created + * @param isEcc Whether this identity was created by setupEccDevice */ - private void trackForCleanup(TestIdentity newIdentity) + private void trackForCleanup(long generation, TestIdentity newIdentity, boolean isEcc) { - // Published for the test body. Never cleared, so a thread the JUnit timeout abandoned can keep reading it. - this.identity = newIdentity; - + boolean superseded; synchronized (lifecycleLock) { - this.identityToDispose = newIdentity; + superseded = generation != this.setupGeneration; + + if (!superseded) + { + this.identityToDispose = newIdentity; + + if (isEcc) + { + this.identityIsEcc = true; + } + } + } + + if (superseded) + { + // A setup the timeout abandoned finished after a later attempt had already started. Publishing now + // would give this instance an identity the running attempt is not using, and lose the one it is. + disposeSupersededIdentity(newIdentity, isEcc); + return; } + + // Published for the test body. Never cleared, so a thread the JUnit timeout abandoned can keep reading it. + this.identity = newIdentity; } /** * Hand a freshly registered ECC device to teardown, so it is removed from the registry even if the rest of * setupEccDevice never completes. * + * @param generation The generation of the setup attempt that registered this device * @param deviceId The device id that was just added to the registry */ - private void trackEccDeviceForCleanup(String deviceId) + private void trackEccDeviceForCleanup(long generation, String deviceId) { + boolean superseded; synchronized (lifecycleLock) { - this.eccDeviceIdToDelete = deviceId; + superseded = generation != this.setupGeneration; + + if (!superseded) + { + this.eccDeviceIdToDelete = deviceId; + } + } + + if (superseded) + { + removeEccDevice(deviceId); + } + } + + /** + * Close and discard an identity produced by a setup attempt that has already been superseded. + * + * @param supersededIdentity The identity to reclaim + * @param isEcc Whether it was created by setupEccDevice, and so must never be recycled + */ + private void disposeSupersededIdentity(TestIdentity supersededIdentity, boolean isEcc) + { + log.debug("Reclaiming identity {} from a superseded setup attempt", supersededIdentity.getDeviceId()); + + if (supersededIdentity.getClient() != null) + { + supersededIdentity.getClient().close(); + } + + if (isEcc) + { + removeEccDevice(supersededIdentity.getDeviceId()); + } + else + { + Tools.disposeTestIdentity(supersededIdentity, iotHubConnectionString); + } + } + + /** + * Remove an ECC device from the registry. These are never recycled: they are self signed with a certificate + * no other test knows about, so returning one to the shared x509 pool would fail a later test. + * + * @param deviceId The device to remove + */ + private void removeEccDevice(String deviceId) + { + try + { + Tools.getRegistyManager(iotHubConnectionString).removeDevice(deviceId); + } + catch (IOException | IotHubException e) + { + log.error("Failed to clean up ECC test device {}", deviceId, e); } } /** - * Dispose anything registered after teardown already ran. + * Dispose anything registered after teardown already ran for this setup's generation. * *

Every test in this class is bounded by a timeout, the two minute one that {@link IntegrationTest} * applies. JUnit runs the test body on a @@ -269,15 +387,21 @@ private void trackEccDeviceForCleanup(String deviceId) * acquiring its identity. Without this, the identity that setup goes on to produce would have no owner and * would leak, which is precisely the leak this class is trying to stop.

* + *

The generation matters. A rerun reuses this instance, so an unqualified "teardown has run" would still + * be set when the next attempt started, and that attempt would dispose its own client the moment it + * published it.

+ * *

This does not wait for setup, in either direction. Blocking teardown on a setup that is itself hung - * which is how these tests have actually timed out - would stall the rest of the run.

+ * + * @param generation The generation of the setup attempt that is finishing */ - private void disposeIfTeardownAlreadyRan() + private void disposeIfTeardownAlreadyRan(long generation) { boolean teardownAlreadyRan; synchronized (lifecycleLock) { - teardownAlreadyRan = this.disposed; + teardownAlreadyRan = this.disposedThrough >= generation; } if (teardownAlreadyRan) @@ -296,16 +420,21 @@ public void dispose() { TestIdentity identityToClean; String eccDeviceIdToClean; + boolean wasEcc; synchronized (lifecycleLock) { - this.disposed = true; + // Teardown covers every setup that has begun so far, and nothing later. A setup that starts after + // this takes a higher generation and is unaffected. + this.disposedThrough = this.setupGeneration; identityToClean = this.identityToDispose; eccDeviceIdToClean = this.eccDeviceIdToDelete; + wasEcc = this.identityIsEcc; this.identityToDispose = null; this.eccDeviceIdToDelete = null; + this.identityIsEcc = false; } if (identityToClean != null && identityToClean.getClient() != null) @@ -319,16 +448,9 @@ public void dispose() // the next test that takes an x509 identity from the shared pool, so delete it instead. This runs even // when the identity was never finished being built, because the device is in the registry from the // moment it is registered, whether or not the rest of the setup succeeded. - try - { - Tools.getRegistyManager(iotHubConnectionString).removeDevice(eccDeviceIdToClean); - } - catch (IOException | IotHubException e) - { - log.error("Failed to clean up ECC test device {}", eccDeviceIdToClean, e); - } + removeEccDevice(eccDeviceIdToClean); } - else if (identityToClean != null && this.identityIsEcc) + else if (identityToClean != null && wasEcc) { // An earlier dispose already claimed and deleted the device id, and this identity was published after // that. The device is gone from the registry, so there is nothing left to delete, but it must still @@ -450,7 +572,12 @@ public void CanOpenConnection() throws Exception InternalClient client = testInstance.identity.getClient(); logConnectionStatusChanges(client); + + // Bracketing the open so a timeout can be attributed. Silence after "Acquiring test identity" and before this + // line means setup was stuck getting an identity; silence after this line means the connect itself stalled. + log.info("Opening client"); client.open(true); + log.info("Client opened"); // deviceClient.open() is a no-op on HTTP, so a message needs to be sent to actually test opening the connection if (testInstance.protocol == HTTPS) @@ -482,7 +609,12 @@ public void CanOpenConnectionWithECCCertificates() throws Exception InternalClient client = testInstance.identity.getClient(); logConnectionStatusChanges(client); + + // Bracketing the open so a timeout can be attributed. Silence after "Acquiring test identity" and before this + // line means setup was stuck getting an identity; silence after this line means the connect itself stalled. + log.info("Opening client"); client.open(true); + log.info("Client opened"); // deviceClient.open() is a no-op on HTTP, so a message needs to be sent to actually test opening the connection if (testInstance.protocol == HTTPS) From 79785f07931dc7ef63690850aec68fe59ea0e9f7 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Sun, 6 Sep 2026 02:18:34 +0000 Subject: [PATCH 5/5] Publish the identity under the lifecycle lock, and stop deleting ECC devices twice Two follow ups on the generation scoped lifecycle. trackForCleanup set identityToDispose under lifecycleLock but assigned the public identity field after releasing it. dispose() is explicitly allowed to run concurrently with setup, so it could interleave between the two writes and claim the identity before the test body could see it. Both writes now happen under the same lock, so what teardown owns and what the test body sees change together. disposeSupersededIdentity deleted the ECC device as well as closing the client. That device is already owned elsewhere: setupEccDevice always registers it and calls trackEccDeviceForCleanup before building the identity, so either that call removed it directly when it was superseded at registration, or the dispose that superseded the attempt removed it. Deleting again logged a not found error over a cleanup that had already succeeded. The superseded path now only reclaims the client. Verified both against the interleavings, including an ECC attempt superseded after registration and one superseded at registration. The double delete reproduces against the previous code and does not against this one. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../iothub/connection/ConnectionTests.java | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/iot-e2e-tests/common/src/test/java/tests/integration/com/microsoft/azure/sdk/iot/iothub/connection/ConnectionTests.java b/iot-e2e-tests/common/src/test/java/tests/integration/com/microsoft/azure/sdk/iot/iothub/connection/ConnectionTests.java index f2b50ae30f..60ce45ccac 100644 --- a/iot-e2e-tests/common/src/test/java/tests/integration/com/microsoft/azure/sdk/iot/iothub/connection/ConnectionTests.java +++ b/iot-e2e-tests/common/src/test/java/tests/integration/com/microsoft/azure/sdk/iot/iothub/connection/ConnectionTests.java @@ -293,6 +293,11 @@ private void trackForCleanup(long generation, TestIdentity newIdentity, boolean { this.identityIsEcc = true; } + + // Published for the test body under the same lock, so that what teardown owns and what the test + // body can see change together. Never cleared, so a thread the JUnit timeout abandoned can keep + // reading it. + this.identity = newIdentity; } } @@ -301,11 +306,7 @@ private void trackForCleanup(long generation, TestIdentity newIdentity, boolean // A setup the timeout abandoned finished after a later attempt had already started. Publishing now // would give this instance an identity the running attempt is not using, and lose the one it is. disposeSupersededIdentity(newIdentity, isEcc); - return; } - - // Published for the test body. Never cleared, so a thread the JUnit timeout abandoned can keep reading it. - this.identity = newIdentity; } /** @@ -351,12 +352,15 @@ private void disposeSupersededIdentity(TestIdentity supersededIdentity, boolean if (isEcc) { - removeEccDevice(supersededIdentity.getDeviceId()); - } - else - { - Tools.disposeTestIdentity(supersededIdentity, iotHubConnectionString); + // Only the client is reclaimed here. setupEccDevice always registers the device and calls + // trackEccDeviceForCleanup before it builds the identity, so the device is already owned by whichever + // path saw it first: trackEccDeviceForCleanup removed it directly if it was superseded at + // registration, and otherwise the dispose that superseded this attempt removed it. Deleting it again + // would just log a not found error over a cleanup that had already worked. + return; } + + Tools.disposeTestIdentity(supersededIdentity, iotHubConnectionString); } /**