Skip to content

[Cosmos] Gateway V2 endpoint-probe: topology-safe gate lifecycle (follow-up to #49437)#49796

Open
jeet1995 wants to merge 154 commits into
Azure:mainfrom
jeet1995:thin-client-probe-flow
Open

[Cosmos] Gateway V2 endpoint-probe: topology-safe gate lifecycle (follow-up to #49437)#49796
jeet1995 wants to merge 154 commits into
Azure:mainfrom
jeet1995:thin-client-probe-flow

Conversation

@jeet1995

@jeet1995 jeet1995 commented Jul 13, 2026

Copy link
Copy Markdown
Member

Context

Follow-up to the review comment thread on #49437. Addresses two endpoint-probe gate lifecycle correctness issues raised in review. Both concern how the thin-client (Gateway V2) routing gate is driven by the background connectivity probe, and both are proven with regression tests in ThinClientProbeWiringTests.


Issue 1 — Overlapping probe fire could cancel the only active probe cycle

Where: GlobalEndpointManager (probe fire/lifecycle) + EndpointProbeClient (single-flight cycle).

The problem. The probe cycle is fired fire-and-forget on every topology refresh, and the live subscription is tracked in an AtomicReference so close() can promptly cancel in-flight probe I/O. The original code tracked the subscription with .set(newDisposable) on every fire. But EndpointProbeClient runs a single-flight cycle: a fire that arrives while a cycle is already running loses the CAS and returns a fast no-op subscription. .set() then overwrote the handle to the real in-flight cycle with that no-op. On close(), getAndSet(null) returned the no-op — so the real probe I/O was never promptly cancelled and only self-terminated on the per-probe timeout. In the worst concurrent case, the single active cycle's cancel handle was lost entirely.

The fix. Keep the tracked handle pointing at the live cycle:

this.thinClientProbeCycleDisposable.getAndUpdate(
    previous -> (previous != null && !previous.isDisposed()) ? previous : newDisposable);

The live predecessor is retained; the new handle is installed only when nothing live is tracked (this fire won single-flight and is the real cycle) or the predecessor is already disposed. The overlapping no-op loser is fire-and-forget and self-completes — no orphaned subscription. close() disposes the real cycle promptly.

Topology freshness is preserved alongside this: every fire (winner or overlapping loser) publishes the newest topology into latestTopology and recomputes the gate before the single-flight CAS, and applyCycleResult recomputes the final gate against latestTopology rather than the winning cycle's captured snapshot. So a topology that advances mid-cycle (e.g. a new region) can never be clobbered by a stale "green" verdict from an older snapshot.

Regression tests.

  • overlappingProbeFire_closeStillCancelsTheLiveCycle — asserts the tracked handle is still the live cycle after an overlapping no-op fire (identity check) and that close() disposes that live cycle. Proven red on the old .set() (fails "overlapping no-op fire must not replace the handle to the live cycle"), green on getAndUpdate.
  • overlappingProbeFire_doesNotCancelActiveCycle — the active cycle survives an overlapping trigger and the gate still flips healthy.
  • staleTopologyGrowth_doesNotFlipGateForUnprobedRegion — a newly added, unprobed region does not get a stale green verdict.

Issue 2 — Init-time opt-out permanently blocked later probe-gated enablement (lazy unset)

Where: RxDocumentClientImpl client-construction wiring of the probe HttpClient into GlobalEndpointManager.

The problem. The probe HttpClient was wired into GEM only when ThinClientConnectivityConfig.canThinClientBeUsed() was true at construction, which consulted COSMOS.IS_THINCLIENT_ENABLED. That flag is tri-state and runtime-mutable (system property / env var), re-read lazily per request: true = force on, false = force off, unset = let the probe verdict gate routing. Gating the wiring on an init-time read meant an explicit false at startup permanently tore out the probe. If an operator later cleared the flag to unset (dropping a hard opt-out to rely on probe-based rollout), there was no live probe to consult, so routing silently pinned to Gateway V1 forever — the dynamic-enablement contract was broken.

The fix. Always wire the probe HttpClient at init and never gate the wiring on the flag:

try {
    this.globalEndpointManager.setThinClientHttpClient(this.reactorHttpClient);
} catch (Throwable t) {
    logger.warn("Failed to wire thin-client connectivity-probe HttpClient; continuing without probe gating.", t);
}

The flag is now honored where it is actually read (lazily, per request): shouldUseThinClientStoreModel returns the explicit verdict directly for true/false and consults the probe decision only when the flag is unset. The probe cycle itself is effectively a no-op whenever the flag is explicitly set — it does real probing only in the unset case where the verdict gates routing. So flipping true/false ↔ unset at runtime now dynamically enables/disables thin-client routing with a live probe already in place, and wiring an explicit opt-in stays free. Wiring is guarded inside GEM so a probe failure can never fail CosmosClient construction.


Testing

  • ThinClientProbeWiringTests — 11/11 green.
  • azure-cosmos build succeeds (revapi NPE trace is known non-fatal noise).

Thin-client connectivity-probe drills — live results (PR #49796 @ d7f284f0)

Ran against thinclient-session (multi-write, EUS2+CUS, enableConnectivityProbe=true) from an EUS2 bench VM. All runs on the implicit probe path (GATEWAY + HTTP/2), single CosmosAsyncClient. Routing was read from the authoritative CosmosDiagnostics.endpoint (host:port) of each request and corroborated with socket state (ss). No account topology was mutated; all faults were client-side on the VM only.

1. Issue 2 — init opt-out must not permanently block probe-gated enablement (COSMOS.THINCLIENT_ENABLED false → unset) ✅ PASS

Single client started with -DCOSMOS.THINCLIENT_ENABLED=false, property cleared at runtime (no restart):

phase COSMOS.THINCLIENT_ENABLED routing
t=0–86s false (hard opt-out) Gateway V1 :443 (eastus2)
t=91s cleared → unset still :443 (probe verdict not yet TRUE)
t=309s unset elevated to Gateway V2 :10250 (thinclient-session-eastus2.documents.azure.com:10250)

Elevation happened ~218s after unset (one topology-refresh probe cycle), on the same client, 0 errors, 0 non-200. This proves the fix: the probe HttpClient is now always wired at init, so dropping a startup false to unset lets the live probe gate routing dynamically. Pre-fix, the probe was never wired under an init opt-out and routing would have pinned to :443 forever.

2. THINCLIENT_PROBE_NETFAULT — C1 V1→V2 elevation ✅ PASS

iptables -A OUTPUT -p tcp --dport 10250 -j DROP applied before bootstrap:

phase routing ss :10250
:10250 blocked Gateway V1 :443 (writes 201/200, reads 200, eastus2) 0 conns
after iptables -D (t=46s)
t=300s elevated to Gateway V2 :10250 (writes+reads 200, eastus2) 8 conns

Probe-gated client under a :10250 block bootstraps on V1; on unblock it elevates to V2 within one probe cycle (~254s), transparently (0 errors).

3. THINCLIENT_PROBE_NETFAULT — C2 regional failover (multi-write) ✅ PASS

Client on V2. DNS-blackholed the write region (thinclient-session-eastus2…192.0.2.1) + severed live :10250 conns:

phase reads writes transport
eastus2 blackholed fail over → central us 200 fail over → central us 200 stays :10250 (V2)
  • Transition: ~100s transient 503 (substatus 10001) burst while in-flight requests to the severed eastus2 :10250 timed out; client marked eastus2 unavailable, refreshed, and converged.
  • Steady state: 74 requests on central us :10250, ss = 16 conns all to CUS IPs (20.15.133.x).
  • 0 :443 downgrades after the fault — failover is regional on the V2 transport, not a V1 fallback (matches the multi-write oracle: reads + writes fail over and succeed).

Full-run endpoint/region tally: 198× :443/eastus2 (V1 blocked) → 22× :10250/eastus2 (V2 elevated) → 74× :10250/centralus (failed over).

jeet1995 and others added 30 commits January 20, 2026 18:20
… QueryPlan proxy routing

Add RNTBD token mappings for x-ms-cosmos-supported-query-features (0x002B)
and x-ms-cosmos-query-version (0x002C) so the thin client proxy can read
these values from the RNTBD body when processing QueryPlan requests.

Previously these headers were only set as HTTP headers by QueryPlanRetriever
and were lost when QueryPlan was routed through the proxy path, since
ThinClientStoreModel serializes requests as RNTBD (not HTTP headers).

IDs match server-side proxy definitions per ADO PR 1982503.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add testThinClientChangeFeedFullRange covering FeedRange.forFullRange()
across multiple partition keys, and testThinClientChangeFeedPartitionKey
covering FeedRange.forLogicalPartition with exact doc count + PK validation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Documents all 59 thin client E2E tests across query (50), point operations (3),
change feed (3), and stored procedures (3) with SQL, query features covered,
and known account-side blockers.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… QueryPlan proxy routing

Add RNTBD token mappings for x-ms-cosmos-supported-query-features (0x00F0)
and x-ms-cosmos-query-version (0x00F1) so the thin client proxy can read
these values from the RNTBD body when processing QueryPlan requests.

IDs are provisional (0x00F0, 0x00F1) — must be coordinated with server-side
proxy team. See ADO PR 1982503 for the proxy-side design.

Note: The design doc listed 0x002B/0x002C but those are already assigned to
PartitionKey/PartitionKeyRangeId in the Java SDK. Using 0x00F0/0x00F1 to
avoid ID collision until final server-side IDs are assigned.

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

- Fix testGetCurrentDateTime: assert ISO 8601 format instead of exact match
  (gateway and proxy return slightly different timestamps)
- Add DefaultAzureCredential support via COSMOS.USE_AAD_AUTH system property
  for accounts with disableLocalAuth=true
- Add RNTBD class reference as .github/instructions/rntbd.instructions.md
- Add pom.xml system properties for THINCLIENT_ENABLED, HTTP2_ENABLED, USE_AAD_AUTH
- Add beforeSuiteReuse mode for degraded accounts

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Switch baseline from Gateway V1 to Direct TCP to avoid JVM config
  interference (THINCLIENT_ENABLED/HTTP2_ENABLED affect Gateway V1)
- Assert :10250 endpoint only on Gateway V2 results (not baseline)
- Rename helpers: assertDirectAndThinClientMatch (was gateway)
- Document seedTestData schema in Javadoc
- Remove 'Expected to fail' comments (account has vector search enabled)
- Clean up class/method Javadoc

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jeet1995 and others added 13 commits July 9, 2026 17:58
Update the gw connCfg assertions to expect gwV2Cto:PT5S instead of
gwV2Cto:n/a, matching the intentional diagnostics change where the
thin-client (gateway V2) connect timeout is emitted by default when
COSMOS.THINCLIENT_ENABLED is unset. This also resolves the cascading
sessionRetryOptionsInDiagnostics failure caused by full() leaking a
system property when its assertion threw before cleanup.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
ThinClient is default-enabled when COSMOS.THINCLIENT_ENABLED is unset on
this branch, so the UserAgent suffix now carries the |F4 feature flag.
Update the stale assertions to mirror RxDocumentClientImpl.addUserAgentSuffix
dynamically (ThinClient default-on + Http2/Http2PingHealth) instead of
hardcoding the suffix.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Refactor EndpointProbeClient to an add-only proven-healthy endpoint cache, matching the .NET implementation (PR azure-cosmos-dotnet-v3#5970). Once an endpoint is proven ThinClient-routable it is never pruned; the routing gate is evaluated against a volatile snapshot of the current topology so a transiently vanished proven region does not gate current routing and is not re-probed when it re-appears. Probe cycles only probe the delta of new, not-yet-proven endpoints on each account refresh.

Add unit tests covering vanish/re-appear and unsupported-new-region scenarios (region added without support blocks the gate; removing it restores routing).

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

Fold ThinClient into generateHttp2OptedInUserAgentIfRequired so the helper
dynamically mirrors production feature-flag suffixing, and remove a
double-wrap of directClientUserAgent (already suffixed at construction).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
On the implicit/probe path (COSMOS.THINCLIENT_ENABLED unset), thin-client
routing depends on the endpoint probe greenlighting all current regions.
Under fault injection the probe is not guaranteed to converge, so requests
may legitimately fall back to Gateway V1 (:443). Assert thin-client routing
only when thin-client is explicitly opted in (THINCLIENT_ENABLED=true), the
sole config where routing is deterministic because the probe is bypassed.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…parsing, probe-client close, fire-and-forget probe cycle, single-flight tests

- Configs: parse COSMOS.THINCLIENT_ENABLED via explicit true/false whitelist
  (parseTriStateThinClientEnabled); any other value logs a warning and is
  treated as unset (null -> probe-gated) instead of Boolean.parseBoolean
  silently collapsing every non-"true" string to a hard opt-out.
- GlobalEndpointManager: close() now cancels the in-flight probe-cycle
  subscription and closes the EndpointProbeClient so a stale cycle drops its
  result. Probe cycle is fired fire-and-forget on force-refresh and topology
  refresh so init()/cross-region retry never block on the probe; routing stays
  on Gateway V1 until the probe proves the proxy endpoints.
- Tests: add ConfigsTests.thinClientEnabledInvalidValueTreatedAsUnset and two
  EndpointProbeClientTests covering single-flight overlap skip and
  closed-mid-cycle result-drop (gate stays conservative).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
useThinClient() now reflects config-eligibility only; the test must gate on actual

per-request routing (read locations + probe/opt-in) to match where the query lands.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…ed()

- Gate probe wiring in RxDocumentClientImpl on canThinClientBeUsed() so an
  explicit THINCLIENT_ENABLED=true still wires the probe (routing bypasses it),
  covering true->null runtime transitions. Removed dead
  canThinClientBeImplicitlyEnabled() from ThinClientConnectivityConfig and
  updated javadoc/comments/pom profile references accordingly.
- Strengthen single-flight test with a post-release third cycle probing a new
  region to confirm the guard releases correctly.
- Skip connectivity-probe requests in GatewayReadConsistencyStrategySpyWireTest
  request selectors: with the probe now wired under explicit enablement, its
  bodyless POST /connectivity-probe to :10250 was being captured by the V2
  selectors, causing an NPE in collectHttpBody. The spy now filters probe
  traffic and selects the actual data request.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…e ReadFeedDatabases cleanup, de-dup endpoint-probe lane

- IncrementalChangeFeedProcessorTest.getCurrentState: block on CFP start()/stop() instead of fire-and-forget subscribe() + fixed sleep, so lease bootstrap failures under shared-account throttling surface deterministically instead of leaving zero leases (size 0 assertion flake).

- ReadFeedDatabasesTest: use CosmosDatabaseForTest.generateId() so leaked DBs are reclaimable by the janitor; rewrite afterClass to delete all DBs in parallel with bounded per-op timeouts and 3x method timeout.

- live-thinclient-endpoint-probe-platform-matrix.json: reduce lane to -Pthinclient-endpoint-probe only, removing -Pquery and -Pconsistency-overrides which duplicated generic suites concurrently against the same shared account.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Mirror safeDeleteDatabase's swallow-everything contract: drop the blockLast(Duration) overload (which throws IllegalStateException on timeout) in favor of blockLast(), rely on the per-op timeout to bound runtime under the method timeout, and add an outer try/catch backstop so no error escapes cleanup.

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

- EndpointProbeClient (Issue #1): recompute the routing gate against the
  latest observed topology (new AtomicReference<Collection<URI>>
  latestTopology) instead of the stale captured snapshot, so a slow
  in-flight cycle can no longer republish a green verdict for a topology
  that has since grown an unproven region. Newly-added regions are proven
  on the next refresh delta.
- GlobalEndpointManager (New-A): fireThinClientProbeCycle now set()s the
  tracked probe Disposable without disposing the previous one; disposal
  happens only in close(), so overlapping refreshes no longer cancel the
  genuine in-flight probe cycle.
- RxDocumentClientImpl (New-B): always wire the thin-client probe HTTP
  client at init; the probe cycle self-no-ops when COSMOS.THINCLIENT_ENABLED
  is explicitly true/false, enabling later probe-gated enablement when unset.
- ThinClientProbeWiringTests: add overlapping-fire and stale-topology-growth
  regression tests (both proven red/green).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0802fe95-dc4d-4ee2-81c8-f707fbffc161
Resolved conflicts in the four probe-flow files by keeping the follow-up
correctness fixes (Issue #1 latest-topology gate, New-A probe-lifecycle,
New-B always-wire) which supersede the merged Azure#49437 baseline in upstream/main.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0802fe95-dc4d-4ee2-81c8-f707fbffc161
@jeet1995
jeet1995 marked this pull request as ready for review July 13, 2026 23:56
Copilot AI review requested due to automatic review settings July 13, 2026 23:56
@jeet1995
jeet1995 requested review from a team and kirankumarkolli as code owners July 13, 2026 23:56
@jeet1995

Copy link
Copy Markdown
Member Author

/azp run java - cosmos - tests

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Follow-up to the Gateway V2 (thin-client) endpoint connectivity-probe rollout work, tightening probe lifecycle correctness under overlapping refreshes and runtime flag transitions to prevent stale “green” gates and inadvertent cancellation of active probe cycles.

Changes:

  • Ensures probe gate computation uses the latest observed topology (prevents stale-green clobbering after topology growth).
  • Adjusts probe-cycle subscription tracking and adds a runtime opt-in/opt-out no-op gate for probe traffic.
  • Adds targeted unit tests for overlapping fires and stale-topology growth scenarios.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/RxDocumentClientImpl.java Always wires the thin-client probe HTTP client at init to avoid permanent disablement from init-time opt-out.
sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/GlobalEndpointManager.java Updates probe fire/disposal behavior and skips probe traffic when COSMOS.THINCLIENT_ENABLED is explicitly set.
sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/EndpointProbeClient.java Recomputes routing gate against the latest topology reference to avoid stale verdict publication.
sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/implementation/ThinClientProbeWiringTests.java Adds regression tests covering overlapping probe fires and topology growth during an in-flight cycle, plus runtime opt-out behavior.

@jeet1995

Copy link
Copy Markdown
Member Author

@sdkReviewAgent

fireThinClientProbeCycle previously called .set(newDisposable) on every
fire. An overlapping fire loses EndpointProbeClient's single-flight CAS and
returns a fast no-op subscription, but .set() still overwrote the handle to
the real in-flight cycle. close() then disposed the no-op, leaving the live
probe I/O to self-terminate only on the per-probe timeout.

Use getAndUpdate to keep the already-tracked live predecessor and only
install the new handle when nothing live is tracked (won single-flight) or
the predecessor is disposed. Adds regression test
overlappingProbeFire_closeStillCancelsTheLiveCycle asserting handle identity
after an overlapping no-op fire and that close() disposes the live cycle.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0802fe95-dc4d-4ee2-81c8-f707fbffc161
@jeet1995

Copy link
Copy Markdown
Member Author

/azp run java - cosmos - tests

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@jeet1995 jeet1995 changed the title [WIP][Cosmos] Gateway V2 endpoint-probe: topology-safe gate lifecycle (follow-up to #49437) [Cosmos] Gateway V2 endpoint-probe: topology-safe gate lifecycle (follow-up to #49437) Jul 14, 2026

@FabianMeiswinkel FabianMeiswinkel left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants