Skip to content

Fix rerun poisoning in ConnectionTests, and move the test template off a retired preview API version - #1869

Open
Ewerton Scaboro da Silva (ewertons) wants to merge 5 commits into
mainfrom
fix-iothub-arm-apiversion
Open

Fix rerun poisoning in ConnectionTests, and move the test template off a retired preview API version#1869
Ewerton Scaboro da Silva (ewertons) wants to merge 5 commits into
mainfrom
fix-iothub-arm-apiversion

Conversation

@ewertons

@ewertons Ewerton Scaboro da Silva (ewertons) commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Two things: a fix for a test harness regression currently on main, and hygiene on the e2e test resource template. The template part does not fix an outage - see below.

Test harness fix

RerunFailedTestRule reuses one test instance for every attempt, so the "teardown has run" flag added in #1863 stayed set across reruns. Every attempt after the first published its fresh client and had it closed immediately, and open() failed with Client was closed while attempting to open the connection. One flaky timeout became a guaranteed failure of all three attempts, which is worse than before #1863.

Build 163484: 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 an earlier attempt's teardown cannot reach a later attempt. The same check makes a superseded setup reclaim what it produced rather than publish it over the identity the running attempt is using.

Also adds logging around identity acquisition and around open. The stalls go silent after "Acquiring test device", which does not separate a setup stuck getting an identity from a connect that never completes.

Carried here rather than in its own PR because this PR cannot go green without it: the regression is on main and lands in this PR's merge commit.

Verified 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.

ARM template hygiene

What changed

  • Microsoft.Devices/IotHubs moves from 2021-03-03-preview to GA 2023-06-30. It was the only preview pin in the template; every other resource was already on a GA version. Preview API versions are retired on a much shorter horizon than GA.
  • StorageEndpoints -> storageEndpoints, the documented casing. ARM matches property names case-insensitively, so file upload has always been configured correctly; the wrong casing was a schema warning, not a defect.
  • test-resources.json is regenerated from the bicep. That surfaced EnableIotHubSecuritySolution, a parameter the bicep had declared since January that the committed JSON was missing, so the two had drifted. It is referenced by nothing - no resource, no output, and not by the deploy script - and no security solution resource has ever existed here, so it is removed at the source rather than published. The emitted template therefore has the same seven parameters as main.

Correcting the original premise

This PR was opened believing the retired preview API version caused the deployCloudTestResources failures on 09-01. That was wrong. main deployed successfully at 163159 and 163283 without this change, and a re-run of another PR on the exact commit that had failed deployed fine with no change at all. The outage was transient and cleared on its own. A retired API version cannot intermittently succeed.

Judge this on the three items above, not as a fix for that.

Verification

  • bicep build succeeds; regenerating the JSON from the bicep reproduces the committed file byte for byte.
  • Diagnostics are 4x BCP073 (tier read-only), identical to what main's template produces with the same tool. The 2x BCP089 that the old casing produced are gone.
  • Emitted template diffed before and after: only the apiVersion string, that one key's casing, and generator metadata differ. Resource list, sku and outputs unchanged.

No Azure subscription access here, so the deployment itself is exercised by the gate rather than locally.

Note on the earlier red check

The Linux failure on this branch was CanOpenConnection[MQTT_WS_SAS_DEVICE_CLIENT_true_true], which had nothing to do with these two files. That test bounded itself with the same 60s the client gives one MQTT CONNECT round trip, so a stalled attempt and the test expired together. Fixed in #1863 and now on main, which this branch has been updated with.

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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The IoT Hub resource still uses StorageEndpoints casing, which does not match the documented storageEndpoints schema for Microsoft.Devices/IotHubs@2023-06-30 and may cause deployment/configuration failures under the new GA API.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Updates the Azure test-resource deployment templates used by deployCloudTestResources to stop using a retired preview IoT Hub API version, unblocking CI resource-group deployments for the Java SDK’s E2E test infrastructure.

Changes:

  • Update Microsoft.Devices/IotHubs from 2021-03-03-preview to GA 2023-06-30 in the Bicep source template.
  • Regenerate the ARM JSON (test-resources.json) from Bicep, updating generator metadata and syncing parameters (including EnableIotHubSecuritySolution with a default of false).
File summaries
File Description
vsts/E2ETestsSetup/test-resources.bicep Updates IoT Hub resource API version to GA 2023-06-30.
vsts/E2ETestsSetup/test-resources.json Regenerated ARM template reflecting the new IoT Hub apiVersion and synced parameter metadata.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread vsts/E2ETestsSetup/test-resources.bicep
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>
@ewertons

Copy link
Copy Markdown
Contributor Author

Correction to post on PR 1869 once GitHub access is restored

My earlier claim was wrong

I said the retired-preview-API diagnosis was "confirmed". It is not. Correcting.

Deploy outcomes for deployCloudTestResources:

Build Pipeline Branch Queued Has fix? Deploy
163048 Windows 1863 09-01 16:50 no failed
163054 Linux main 09-01 17:25 no failed
163055 Windows main 09-01 17:25 no failed
163057 Android main 09-01 17:25 no failed
163073/74/75 all three 1863 09-01 17:37 no failed
163159 Linux main 09-02 02:00 no SUCCEEDED
163172 Android main 09-02 06:00 no failed
163173 Windows main 09-02 06:00 no failed
163194/95/96 all three 1869 09-02 13:15 yes succeeded
163283 Linux main 09-03 02:00 no SUCCEEDED

163159 and 163283 are the problem for my theory: main, without this change, deploying fine. A retired API version cannot intermittently succeed. So the failures were intermittent and the retirement diagnosis is probably wrong.

The inner BadRequest message is truncated in the task log at a fixed length, so the actual cause is still unread. Teardown succeeded in every affected build, so orphaned resources hitting a quota does not explain it either.

The clean control is the next main Windows/Android nightly, which runs without this change. If its deploy succeeds, this PR fixed nothing.

What this PR is actually worth

Judge it as hygiene, not as an outage fix:

  • 2021-03-03-preview -> 2023-06-30 GA. Preview versions do get retired; being the only preview pin in the template was a real risk regardless of whether it caused this.
  • StorageEndpoints -> storageEndpoints, the documented casing. Clears both BCP089 warnings.
  • Regenerating the JSON also syncs EnableIotHubSecuritySolution, which the bicep has declared since January and the committed JSON was missing. Unreferenced, defaults to false.

Verified inert: emitted template diffed before/after is identical apart from the apiVersion string and that one key's casing. Resource list, sku and outputs unchanged. Bicep diagnostics identical to main built with the same tool (4x BCP073), so this introduces none.

I have no Azure access here, so the deployment itself is exercised only by the gate.

The Linux failure on this PR

163194 failed one test, CanOpenConnection[MQTT_WS_SAS_DEVICE_CLIENT_true_true], 60s timeout on JDK 21. Windows 163195 and Android 163196 passed.

This PR changes 2 ARM template files and no Java, gradle, pom, PowerShell, YAML or shell. It cannot affect a Java test.

That test is the dominant failure on main: over the last 13 main Linux builds, 8 failed, and 6 of those involved CanOpenConnection[MQTT_WS_SAS_DEVICE_CLIENT_true_*] (163159, 162834, 162803, 162703, 162611, 162121). Needs a re-run.

@ewertons

Copy link
Copy Markdown
Contributor Author

Why Linux JDK 21 failed here

I called this "a known flake" without reading it. That was lazy. It has a specific cause, and it is not random.

ConnectionTests.CanOpenConnection:277 -> Object.wait:366 -> Object.wait0 » TestTimedOut
test timed out after 60000 milliseconds   (3 attempts, 180s total)
Tests run: 497, Failures: 0, Errors: 1, Skipped: 339

Line 277 on main is open(true). The thread is parked in Object.wait inside the open, so the connection never establishes; it is not the close.

The variant is [MQTT_WS_SAS_DEVICE_CLIENT_true_true]. Those trailing flags are useHttpProxy=true, useHttpProxyAuth=true, so it runs through the authenticated proxy the test class starts locally.

It is the proxied variants, every time

ConnectionTests has 28 parameter combinations: 12 proxied, 16 not. Every CanOpenConnection failure in the recent Linux builds is a proxied one - 163194, 163159, 162834, 162803, 162703, 162611, 162121, all _true_false or _true_true. Zero non-proxied failures, from the larger half of the matrix. Random flakiness would put roughly 57% of them in the non-proxied variants.

Why this branch is exposed

This PR branches from main, and on main that class still has both defects:

  • proxyServer.startAsync(port) on lines 217 and 222, return value discarded. The future that completes when the port is listening is thrown away, so a test can send traffic to a proxy that is not accepting yet.
  • dispose() is defined on line 184 and has 0 callers. There is no import org.junit.After in the file. Every client the class opens is leaked and keeps retrying through those same local proxies for the life of the JVM.

Both are exactly what #1863 fixes: on that branch ProxyServerTools.startProxyServer appears twice and testInstance.dispose() is called from an @After.

So this is not this PR's doing - it changes 2 ARM template files, no Java, gradle, pom, PowerShell, YAML or shell - and it is not unexplained. It is a real defect in the test harness that is fixed in #1863 and not yet merged.

Ordering note: #1863 is currently blocked behind this PR, since its Windows and Android jobs were failing in resource deployment before reaching any test. Whether that deployment failure was actually fixed by this change is still unproven - see my previous comment; main has since deployed successfully twice without it.

@ewertons

Copy link
Copy Markdown
Contributor Author

The Linux failure here is not stale, but it is the only thing red

Checks on this PR point at exactly three builds, all from 09-02 13:15 on 75d0070b4, and no build has run since:

Pipeline Build Result
Java Linux 163194 failed
Java Windows 163195 succeeded
Java Android 163196 succeeded

Within 163194, Linux JDK 21 is attempt 1 with no retry. Its sibling jobs - JDK 8, 11, 17, deploy and teardown - all passed, which is probably what looked like new passing runs. Only JDK 21 failed, on CanOpenConnection[MQTT_WS_SAS_DEVICE_CLIENT_true_true].

The premise for this PR is now disproven

I said earlier this was unproven. It is now settled, against it.

PR #1863 was re-run on the same commit that had failed in resource deployment, with no change of any kind, and deployCloudTestResources succeeded on both Windows 163288 and Android 163289 - the exact step that had failed on 163074 and 163075. main had already deployed cleanly at 163159 and 163283.

So the deployment outage was transient and cleared on its own. This change did not fix it.

What this PR is still worth

Hygiene, and it should be judged only as that:

  • Microsoft.Devices/IotHubs moves off 2021-03-03-preview, the only preview pin in the template, onto GA 2023-06-30. Preview versions do get retired eventually; that risk was real even though it is not what happened here.
  • StorageEndpoints -> storageEndpoints, the documented casing, clearing both BCP089 warnings.
  • Regenerating the JSON syncs EnableIotHubSecuritySolution, which the bicep has declared since January and the committed JSON was missing.

Verified inert: emitted template is identical apart from the apiVersion string and that one key's casing; resource list, sku and outputs unchanged; bicep diagnostics identical to main built with the same tool.

Reasonable to close this if the drift and the preview pin are not considered worth carrying on their own.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The changes are low-risk template hygiene (GA apiVersion + casing + regeneration) and do not introduce functional logic changes that would block merging.

Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread vsts/E2ETestsSetup/test-resources.json Outdated
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>
@ewertons

Copy link
Copy Markdown
Contributor Author

The Linux JDK 21 failure is not from this PR, and it is a regression in main

Windows 163486 and Android 163487 passed. Linux 163484 failed on tokenRenewalWorks and CanOpenConnection[MQTT_WS_SAS_DEVICE_CLIENT_true_false]. This PR changes two ARM template files and no Java, so it cannot affect either.

The CanOpenConnection failure exposes a defect in #1863, which I wrote and which is now on main:

03:35:27.194  Starting test: CanOpenConnection[MQTT_WS_SAS_DEVICE_CLIENT_true_false]
03:35:27.197  Acquiring test device from testSasDeviceQueue
              ... 120s, no connection status transition ...
03:37:27.200  Test failed on run 1, test timed out after 120000 ms. Rerunning.
              ... 120s ...
03:39:27.202  Test failed on run 2, test timed out after 120000 ms. Rerunning.
03:39:27.203  Acquiring test device from testSasDeviceQueue
03:39:27.768  Closing device client...              <-- 0.5s in, before any connect
03:40:27.205  Device client closed                  <-- 60s in close()
03:40:27.205  Test failed on final rerun

Two separate things.

The rerun poisoning is mine. ConnectionTestInstance.disposed is set in dispose() and never reset, and RerunFailedTestRule reuses the same instance across attempts. So once the first attempt's @After has run, every later attempt calls setup(), hands its fresh client to trackForCleanup, and disposeIfTeardownAlreadyRan() immediately closes it - which is the 0.5s "Closing device client" above. open(true) then fails with Client was closed while attempting to open the connection. A single flaky timeout is converted into a guaranteed three attempt failure. That is worse than the behaviour before #1863 and needs fixing on main.

The underlying stall is not fixed. Run 1 had no prior dispose, so it is unaffected by the above, and it still burned 120s with no status transition. Raising the bound from 60s to 120s did not make this survivable, which is what I hoped it would do. I said at the time that it made a stall survivable rather than stopping stalls; that claim was too optimistic for a stall of this length.

Neither belongs on this PR. I will raise the rerun fix separately against main.

…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>
@ewertons Ewerton Scaboro da Silva (ewertons) changed the title Move the IoT hub in the test template off a retired preview API version Fix rerun poisoning in ConnectionTests, and move the test template off a retired preview API version Sep 5, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The updated ConnectionTests lifecycle code still has concurrency/cleanup edge cases (atomic publication under lock and avoiding double ECC deletion) that should be addressed to prevent flaky behavior and misleading CI errors.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment on lines +288 to 309
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;
}
Comment on lines +352 to +359
if (isEcc)
{
removeEccDevice(supersededIdentity.getDeviceId());
}
else
{
Tools.disposeTestIdentity(supersededIdentity, iotHubConnectionString);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants