Skip to content

Gate thin-client (Gateway V2) routing behind an endpoint connectivity probe when COSMOS.THINCLIENT_ENABLED is unset#49437

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

Gate thin-client (Gateway V2) routing behind an endpoint connectivity probe when COSMOS.THINCLIENT_ENABLED is unset#49437
jeet1995 wants to merge 149 commits into
Azure:mainfrom
jeet1995:jeet1995/thin-client-probe-flow

Conversation

@jeet1995

@jeet1995 jeet1995 commented Jun 10, 2026

Copy link
Copy Markdown
Member

Motivation

Gateway V2 (ThinClient) is the new Compute-hosted data plane for Cosmos DB. To roll it out safely the Java SDK needs a user-transparent way to route eligible data-plane and QueryPlan requests to ThinClient only once the proxy is proven reachable (or the user explicitly opts in) — without user-visible errors or per-request overhead. Today the SDK has no client-side reachability signal which this PR aims to solve:

  • Making COSMOS.THINCLIENT_ENABLED an explicit tri-state: true = hard opt-in (probe bypassed), false = hard opt-out, unset = probe-gated (route to Gateway V2 only once the connectivity probe greenlights the proxy, else Gateway V1). Unset does not default routing on.
  • Adding an out-of-band connectivity probe (EndpointProbeClient) that POSTs to /connectivity-probe over the thin-client HTTP/2 transport every topology refresh. The verdict feeds the routing gate via GlobalEndpointManager.getProxyProbeDecision().

Key invariants:

  • No per-request probe I/O — probing piggybacks on the topology-refresh cadence; the data-plane gate is a single in-memory read.
  • Conservative implicit path — with the flag unset, before any region is proven the gate is false (empty endpoint set), so a slow/failing first probe never pushes traffic onto an unproven proxy.
  • Explicit opt-in bypasses the probeTHINCLIENT_ENABLED=true skips the probe gate for routing; the probe governs only the implicit path.
  • Never trip client init — every probe failure is swallowed and logged; client construction and topology refresh are protected by onErrorResume.
  • Tri-state gate, no oscillation countersgetProxyProbeDecision() is true only when every known region has a proven (HTTP 200) probe, false when a known region is unproven/non-200, null before any verdict. On the implicit path both false and null pin to Gateway V1; a region flips healthy on its first 200 — no thresholds.
  • Add-only proven-healthy cache — a 200 records a region proven and is never pruned. Each cycle probes only the delta; the gate is provenHealthyEndpoints.containsAll(currentEndpoints) against a volatile snapshot.

Call sequence

sequenceDiagram
    autonumber
    participant App as User App
    participant Client as CosmosClient<br/>(RxDocumentClientImpl)
    participant GEM as GlobalEndpointManager
    participant Probe as EndpointProbeClient
    participant Proxy as ThinClient<br/>(Gateway V2)
    participant V1 as Gateway V1<br/>(fallback)

    rect rgba(200,220,255,0.25)
    Note over App,Probe: Bootstrap (one-time)
    App->>Client: new CosmosClient(...)
    Client->>Client: canThinClientBeUsed() (HTTP/2 + Gateway mode, not opted out)
    alt thin-client usable
    Client->>GEM: setThinClientHttpClient(...)
    GEM->>Probe: new EndpointProbeClient(httpClient)
    Note over Probe: wired even for explicit true — routing bypasses it on that path
    else not usable
    Note over Client,Probe: probe not wired
    end
    end

    rect rgba(200,255,210,0.25)
    Note over GEM,Proxy: Topology refresh (every tick)
    GEM->>GEM: refreshLocationPrivateAsync() → runThinClientProbeCycleMono()
    GEM->>Probe: runProbeCycle(delta endpoints)
    par Per new endpoint
        Probe->>Proxy: POST /connectivity-probe (HTTP/2)
        Proxy-->>Probe: 200 (or error/timeout)
    end
    Probe->>Probe: applyCycleResult() — cache proven regions
    Note right of Probe: all regions 200 → true — any unproven/non-200 → false
    end

    rect rgba(255,235,200,0.25)
    Note over App,V1: Data-plane / QueryPlan request
    App->>Client: readItem(...) / queryItems(...)
    Client->>GEM: getProxyProbeDecision()
    GEM-->>Client: true / false / null
    alt usable && hasThinClientReadLocations && eligible && (explicitOptIn || decision==true)
        Client->>Proxy: route through Gateway V2
    else gate failed (false/null, implicit path)
        Client->>V1: route through Gateway V1
    end
    Client-->>App: response (transparent fallback)
    end
Loading

Configuration

Property Default Purpose
COSMOS.THINCLIENT_ENABLED unset (tri-state) Master switch (getter: Configs.isThinClientEnabled()). Unset ⇒ probe-gated; true ⇒ opt-in bypassing the probe; false ⇒ never route thin-client.

This PR only changes the semantics of the master switch; HTTP/2 (COSMOS.HTTP2_ENABLED) + GATEWAY mode remain the transport prerequisite. /connectivity-probe is a fixed proxy contract — no separate kill-switch, threshold, or probe-path property.

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>
@xinlian12

Copy link
Copy Markdown
Member

Review complete (46:59)

Posted 6 inline comment(s).

Steps: ✓ context, correctness, cross-sdk, design, history, past-prs, synthesis, test-coverage

jeet1995 and others added 3 commits July 9, 2026 21:44
…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>
@jeet1995

Copy link
Copy Markdown
Member Author

/azp run java - cosmos - kafka

@jeet1995

Copy link
Copy Markdown
Member Author

/azp run java - cosmos - spark

@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).

2 similar comments
@azure-pipelines

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

@azure-pipelines

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

jeet1995 and others added 3 commits July 10, 2026 11:53
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>
@jeet1995

Copy link
Copy Markdown
Member Author

/azp run java - cosmos - tests

@jeet1995

Copy link
Copy Markdown
Member Author

/azp run java - cosmos - spark

@jeet1995

Copy link
Copy Markdown
Member Author

/azp run java - cosmos - kafka

@azure-pipelines

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

2 similar comments
@azure-pipelines

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

@azure-pipelines

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

…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>
@jeet1995

Copy link
Copy Markdown
Member Author

/azp run java - cosmos - tests

@jeet1995

Copy link
Copy Markdown
Member Author

/azp run java - cosmos - spark

@jeet1995

Copy link
Copy Markdown
Member Author

/azp run java - cosmos - kafka

@azure-pipelines

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

2 similar comments
@azure-pipelines

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

@azure-pipelines

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

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