Skip to content

Fix Data Explorer health scenarios reporting false unhealthy loads - #2590

Open
Dmitrii Shilov (bk201-) wants to merge 7 commits into
masterfrom
dev/dshilov/fix-scenario-phase-ordering
Open

Dmitrii Shilov (bk201-) wants to merge 7 commits into
masterfrom
dev/dshilov/fix-scenario-phase-ordering

Conversation

@bk201-

Copy link
Copy Markdown
Contributor

ScenarioMonitor.completePhase() silently no-oped when the phase had not been started yet, and the production call ordering in Explorer.tsx / ResourceTree.tsx always hits that case. DataExplorerHealthV2 therefore reported loads as unhealthy that had in fact completed.

Evidence

Over three days, scenario_timeout events:

Scenario Timeouts Dominant missing phase Share
DatabaseLoad 1627 DatabaseTreeRendered 1624 (99.8%)
ApplicationLoad 649 PlatformConfigured + ExplorerInitialized 649 (100%)

documentHidden was true in 692 of 2276 (30%) — a contributing factor, not the cause; the same pattern dominates in foreground tabs.

Root cause

DatabaseTreeRendered is started at Explorer.tsx:451, immediately after completePhase(CollectionsLoaded). The effect that completes it (useMetricPhases.ts:55-59) re-fires only on a databaseTreeNodes change, and databaseTreeNodes is memoised (ResourceTree.tsx:54). The last change happens before line 451, so the effect never re-fires and the phase stays open until the 10s timeout. This is a deterministic ordering inversion, not a race — which is why it is 99.8% and not 50%.

Interactive is completed by a one-shot effect whose deps ([scenario, enabled]) are stable. If ResourceTree mounts before Explorer.tsx:577 starts the scenario, the completion is dropped and never retried. Missing in 588 of 1627.

ApplicationLoad never got past configurePortal(), which resolves only from the iframe message handler with no timeout and no reject. When the portal does not post the init message the promise stays pending forever — the user sits on <LoadingExplorer /> and the scenario times out with no diagnostic. Both phases hang off the same await, which is why they are always missing together.

Changes

  • completePhase self-starts a required phase that is completed before startPhase, instead of dropping the completion.
  • Completions reported before the scenario exists are buffered and replayed on start().
  • A timeout while the tab is backgrounded no longer counts against health. Browsers throttle timers and suspend rAF there, so phase completion is unreliable and the elapsed time is not the user's experience. documentHidden is still reported, so these remain sliceable in telemetry.
  • configurePortal() rejects after 30s with a ConfigurePortal failure trace — deliberately well above the 10s scenario budget so only genuinely stuck handshakes fail. The caller now handles the rejection instead of leaving it unhandled.

Tests

src/Metrics/ScenarioMonitor.phaseOrdering.test.ts replays the exact production call ordering and asserts it now completes healthy, plus: an early completion before start(), a genuine stall still reporting unhealthy, and a backgrounded-tab timeout not counting against health.

Locally src/Metrics/ is 53 passed (4 new + 49 existing), and tsc --noEmit reports no errors in src/.

Notes for reviewers

  • CI may be red for unrelated reasons. master bumped cheerio from 1.0.0-rc.12 to 1.2.0, and enzyme@3.11.0 requires cheerio/lib/utils, which no longer exists. Since jest.config.js:133 loads enzyme-to-json/serializer for every suite, no test in the repo currently runs on a clean install. I validated with a temporary config that drops the enzyme serializer; that config is not part of this PR.
  • On npm 12 install scripts are blocked by default, so canvas does not build and jsdom fails to load. The repo has no allowScripts field in package.json yet — worth adding separately.
  • Not included: an error state instead of the indefinite <LoadingExplorer />. That needs new localized strings and is better as its own change.

The sending half of this handshake is fixed separately in CosmosDB-portal (PR 2294596): _fetchMasterKey / _getAuthorizationToken had no rejection handler, so the init message was never posted.

Related: IcM 865096261

ScenarioMonitor.completePhase() silently no-oped when the phase had not been
started yet, and the production call ordering always hits that case:

- DatabaseTreeRendered is started in Explorer.tsx after the last databaseTreeNodes
  change, but the effect that completes it only re-fires on such a change and
  databaseTreeNodes is memoised, so the phase never closed.
- Interactive is completed by a one-shot effect whose deps are stable, so it is
  lost entirely when ResourceTree mounts before refreshExplorer starts the
  scenario.

Over three days DatabaseTreeRendered was missing in 1624 of 1627 DatabaseLoad
timeouts (99.8%) and Interactive in 588, driving DataExplorerHealthV2 to report
loads as unhealthy that had in fact completed.

completePhase now self-starts a required phase that is completed before
startPhase, and completions reported before the scenario exists are buffered and
replayed on start(). A timeout while the tab is backgrounded no longer counts
against health either: browsers throttle timers and suspend rAF there, so phase
completion is unreliable and the elapsed time is not the user's experience.
documentHidden is still reported so those can be sliced out in telemetry.

Separately, configurePortal() resolved only from the iframe message handler with
no timeout. When the portal never posts the init message the promise stayed
pending forever, leaving the user on LoadingExplorer with no diagnostic while
ApplicationLoad timed out mute. It now rejects after 30s with a ConfigurePortal
failure trace, and the caller handles the rejection instead of leaving it
unhandled.

Related: IcM 865096261
@bk201-
Dmitrii Shilov (bk201-) requested a review from a team as a code owner September 10, 2026 14:59
@github-actions

Copy link
Copy Markdown

Playwright tests ❌ failed

Passed Failed Flaky Duration
668 1 18 1484s

📁 Report: 34492648928-1/report.zip
Open container (Azure sign-in required) → click into 34492648928-1 folder → click report.zip → Download → unzip → open index.html · Workflow run

⚠️ 1 shard(s) failed before tests ran (infra/auth issue). Stats below reflect only shards that executed.

Keep confirmation popovers above adjacent accordion headers. Target the actual switch, mock destination role APIs with ARM response shapes, and synchronize PITR and role assignment completion. Fix initialization hook formatting.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown

Playwright tests ✅ passed

Passed Failed Flaky Duration
679 0 8 627s

📁 Report: 34502953572-1/report.zip
Open container (Azure sign-in required) → click into 34502953572-1 folder → click report.zip → Download → unzip → open index.html · Workflow run

@sunghyunkang1111

sunghyunkang1111 commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

1. Overview

Reducing false-unhealthy emissions is the right goal, and neither this PR nor the companion portal handshake fix needs to eliminate every transient failure. However, the reviewed patch also permits false-healthy outcomes and can prevent late initialization from recovering. Those are code-correctness issues to address before merge. Deciding which remaining failures warrant an IcM is separate monitoring work.

The concern is that some changes make the observed timeout patterns disappear without establishing that the load finished. A lower unhealthy percentage alone would not demonstrate that the fix worked.

2. What needs to be fixed in this PR

  1. Preserve late initialization recovery. If a valid init message arrives but key retrieval takes longer than 30 seconds, Promise.race rejects the caller. The message handler remains active: when the dependency completes, it can still create an Explorer and emit ConfigurePortal success, but the rejected caller never resumes its normal setExplorer path. Previously, the pending initialization could recover. This is a functional regression path, not residual alert noise. Catch errors inside the async handler and give initialization a consistent lifecycle: either allow recovery through the waiting caller, or cancel/ignore late work and expose an error/retry state.

  2. Do not accept an earlier render as proof that the loaded tree is ready. The early-completion buffer and phase auto-start can accept a tree render before collections finish. Completing the remaining phases then reports healthy with a zero-duration DatabaseTreeRendered, without waiting for a render of the collection-bearing tree. That removes the missing-phase signature by accepting an insufficient observation. Fix the producer ordering and acknowledge the committed tree for the current load's ready revision. Cover stale callbacks, empty accounts and unchanged trees; checking only that a healthy event was emitted is insufficient.

  3. Do not turn unfinished background loads into successes. A load with no completed phases can time out while hidden and still emit healthy=true under this patch. Background timer throttling can justify different alert eligibility, but it does not establish successful initialization. Preserve the actual timeout/completion outcome and visibility context. Let the monitor apply a consistent background policy rather than lowering the failure rate by relabeling incomplete loads.

These are source-level reproductions, and the relevant behavior is unchanged at reviewed head 75d2fc20. Their production frequency is not established. The first breaks recovery; the other two weaken what a healthy outcome proves.

3. What belongs in monitor and alerting changes

  • Decide which recorded failures are actionable. The observed 88.4 ms fetch failure can remain an error without necessarily opening an IcM. Use persistence, failure counts and appropriate volume safeguards to reduce incidents from isolated failures.
  • Apply background eligibility consistently to successful and failed outcomes. Retain raw failures and deadline misses even when alert policy excludes them.
  • Avoid a blanket sample floor: 72.9% of five-minute windows in the combined-scenario Stage log replay had fewer than 20 records. Preserve low-traffic outage and no-data detection.
  • Retrieve the actual Stage V2 rule and explain MDM's original 2/19 versus the backend logs' 1/9 before calibrating thresholds. That discrepancy is monitoring follow-up, not a blanket blocker for a scoped, correct code improvement.

The previous revision removed the missing-phase signatures, but three of those
changes could report a load as healthy without establishing that it finished,
and one could prevent a slow initialization from recovering.

Preserve late initialization recovery. configurePortal() raced the iframe
handshake against a 30s reject while leaving the message listener active, so a
late init message would build an Explorer that the rejected caller never
received — turning a slow load into an unrecoverable one. The timeout is now a
watchdog that traces the stuck handshake and lets the caller keep waiting, so
recovery behaves as it did before. Initialization work inside the message
handler is wrapped so a throw reports and still yields a shell, instead of
leaving the promise pending forever.

Do not accept an earlier render as proof the loaded tree is ready. Phase
auto-start let a render of the databases-only tree complete DatabaseTreeRendered
with a zero-duration measurement, before collections had loaded. The producer
ordering is fixed instead: Explorer publishes a ready revision once the load has
produced the data the tree should show, and ResourceTree completes the phase only
for a render carrying that revision, acknowledging each revision once. Stale
callbacks from a superseded load, accounts with no databases and unchanged trees
are covered by tests. A completion for a phase that was never opened is now
refused and reported as phase_complete_unstarted rather than backdated, and the
early-completion buffer no longer accepts deferred phases, which must be opened
by their producer.

Do not turn unfinished background loads into successes. A timeout in a hidden tab
emitted healthy=true even with no phases completed. The emitted outcome again
reflects what actually happened; documentHidden continues to be reported so
alerting can apply a background policy without the load being relabelled.

Related: IcM 865096261
@github-actions

Copy link
Copy Markdown

Playwright tests ✅ passed

Passed Failed Flaky Duration
673 0 14 953s

📁 Report: 34830743602-1/report.zip
Open container (Azure sign-in required) → click into 34830743602-1 folder → click report.zip → Download → unzip → open index.html · Workflow run

The ready signal was an incrementing counter, so its correctness rested on an
argument about scale rather than a property of the value. Overflow was not
reachable in a page session — nine quadrillion refreshes — and the failure mode
was fail-closed, leaving the phase open rather than reporting a load as healthy.
It was still a bound that had to be argued rather than one that could not be
exceeded.

The signal is now a fresh object published per load and compared by reference.
There is no value to exhaust or wrap, and the comparison is identity rather than
ordering, so a store reset cannot leave the consumer permanently ahead of the
producer either.

Related: IcM 865096261
@github-actions

Copy link
Copy Markdown

Playwright tests ❌ failed

Passed Failed Flaky Duration
661 6 18 1497s

📁 Report: 34836629179-1/report.zip
Open container (Azure sign-in required) → click into 34836629179-1 folder → click report.zip → Download → unzip → open index.html · Workflow run

⚠️ 2 shard(s) failed before tests ran (infra/auth issue). Stats below reflect only shards that executed.

@github-actions

Copy link
Copy Markdown

Playwright tests ✅ passed

Passed Failed Flaky Duration
675 0 12 668s

📁 Report: 34951145063-1/report.zip
Open container (Azure sign-in required) → click into 34951145063-1 folder → click report.zip → Download → unzip → open index.html · Workflow run

# Conflicts:
#	test/sql/containercopy/permissionsScreen.spec.ts
@github-actions

Copy link
Copy Markdown

Playwright tests ✅ passed

Passed Failed Flaky Duration
437 0 7 833s

📁 Report: 36574916476-1/report.zip
Open container (Azure sign-in required) → click into 36574916476-1 folder → click report.zip → Download → unzip → open index.html · Workflow run

@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown

Playwright tests ✅ passed

Passed Failed Flaky Duration
441 0 3 1404s

📁 Report: 37341195977-1/report.zip
Open container (Azure sign-in required) → click into 37341195977-1 folder → click report.zip → Download → unzip → open index.html · Workflow run

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants