Skip to content

Perf: Fast Cache Approach - Exploring Possible Optimisations in the Token Acquisition Process - #2982

Open
Divyansh-jain2 wants to merge 5 commits into
microsoft:mainfrom
Divyansh-jain2:users/divyansh/fast-cache
Open

Perf: Fast Cache Approach - Exploring Possible Optimisations in the Token Acquisition Process#2982
Divyansh-jain2 wants to merge 5 commits into
microsoft:mainfrom
Divyansh-jain2:users/divyansh/fast-cache

Conversation

@Divyansh-jain2

@Divyansh-jain2 Divyansh-jain2 commented Jul 9, 2026

Copy link
Copy Markdown

Description

When connecting with authentication=ActiveDirectoryServicePrincipal (and the other AAD principal flows that go through SQLServerMSAL4JUtils.getSqlFedAuthTokenPrincipal), every token acquisition — including cache hits — is currently serialized behind a global Semaphore(1) keyed on the principal. The semaphore exists to stop a burst of cache misses from stampeding the Entra ID token endpoint, but it also forces threads that only need to read an already-cached token to queue one-at-a-time behind whoever currently holds the permit.

Under load where many connections share the same service principal, this turns a cheap in-memory cache read into a serialization point: N concurrent connections that could all be satisfied from the MSAL cache instead drain through a single permit.

This change adds a lock-free silent fast-path that attempts a cache-only token read before taking the semaphore. Cache hits return immediately and in parallel; only cache misses fall through to the unchanged, semaphore-gated slow path that protects the token endpoint.

How it works

acquireTokenSilently(...) is MSAL's cache-only lookup — it checks the client application's in-memory token cache (rehydrated here from the persistent cache aspect) for a still-valid token for the requested scopes and returns it without contacting Entra ID. If nothing valid is cached, its future fails with an ExecutionException, which we treat as a miss and fall through to the existing gated path.

The gated slow path is unchanged and remains safe: the first caller through the gate refreshes the token and populates the shared cache; callers that were waiting on the gate then get that freshly-cached token because acquireToken(...) does its own cache lookup first (skipCache defaults to false), so they don't make a redundant AAD call.

Before:  [all connections] ──> Semaphore(1) ──> cache check / AAD ──> token
                                    ▲ cache hits queue here too

After:   cache hit  ──> return immediately (no semaphore)
         cache miss ──> Semaphore(1) ──> AAD refresh ──> token   (unchanged)

Net effect: cache hits are served concurrently; the token endpoint keeps the same stampede protection for misses.

Testing

Validated with a standalone single-service-principal Token Acquisiton benchmark, included for reference (not part of the build) at docs/performance/TokenAcquisitionTest.java.

What the harness does

  • Spins up K worker threads that each open a brand-new JDBC connection to the same service principal, run SELECT 1, close it, and repeat in a tight loop for T seconds. Sharing one credential across all threads is the worst-case head-of-line / token-cache contention scenario, which isolates exactly what this change affects.
  • All workers are released simultaneously via a CountDownLatch so they contend at once rather than ramping up.
  • Runs a warm-up phase first (primes the MSAL token, TLS and DB caches, then rests) so the measured window consists of steady-state cache hits; warm-up metrics are discarded. A cold mode is also available to measure the cache-miss stampede.
  • Registers a PerformanceLogCallback that captures the driver's internal TOKEN_ACQUISITION timing for every connection and reports count / avg / max, accumulated lock-free so the measurement adds negligible overhead.
  • Reports throughput (connections/sec), avg/max end-to-end latency, and failures alongside the token-acquisition numbers.

How it's run

The same command is used twice, changing only which driver jar is on the classpath (before vs. after this change). The driver-org / fast-cache argument is just a label printed in the output.

# baseline: the current driver jar
java -cp "mssql-jdbc-<version>.jar;." TokenAcquisitionTest driver-org <K> <T> warm

# this change: the driver jar built from this branch
java -cp "mssql-jdbc-<version>.jar;." TokenAcquisitionTest fast-cache <K> <T> warm

The file will not compile inside this repo by design — it depends on a small, local, git-ignored Creds helper that the reader supplies with their own Azure SQL server FQDN, database name and service-principal id/secret. No credentials are embedded; it is kept purely so reviewers can see exactly how the numbers below were produced.

Results — token acquisition average latency (warm cache; lower is better):

Measured window driver-org (baseline) fast-cache Change
10 s 87.3 ms 38.3 ms −56.1%
60 s 97.2 ms 40.6 ms −58.2%
300 s 95.5 ms 35.7 ms −62.6%
600 s 93.8 ms 37.8 ms −59.7%

The improvement reflects cache hits no longer queuing behind the single semaphore permit. Cache-miss behavior (the AAD refresh path) is unchanged.

Guidelines

  • The code builds clean without any errors or warnings
  • I have followed the coding style guidelines of this project
  • Backwards compatible: no public API changes; miss path (AAD stampede protection) unchanged
  • I have added tests / the change is covered by existing behavior (see note below)
  • CHANGELOG.md updated (will add once the PR number is assigned)

In getSqlFedAuthTokenPrincipal, attempt acquireTokenSilently (an MSAL cache-only read that never contacts Entra ID) before taking the global token semaphore. A cache hit returns immediately without contending for the gate, eliminating head-of-line blocking where in-memory token reads queued behind threads refreshing tokens. A cache miss falls through to the unchanged gated slow path, preserving the existing AAD-stampede protection.

The silent read is extracted into a package-private helper acquireTokenSilentlyOrNull for unit testing. Adds SQLServerMSAL4JUtilsTest coverage for the cache-hit (returns token, no AAD call) and cache-miss (returns null, falls through) paths using Mockito.
…silent fast-path

Re-extract the ActiveDirectoryServicePrincipal silent cache read into the package-private helper acquireTokenSilentlyOrNull so the cache-hit and cache-miss paths can be unit-tested in isolation. Behavior is unchanged: a hit returns the cached token without taking the semaphore; a miss returns null and falls through to the gated slow path.

Adds SQLServerMSAL4JUtilsTest coverage: testAcquireTokenSilentlyReturnsCachedTokenOnHit (cache hit returns the token and never calls acquireToken) and testAcquireTokenSilentlyReturnsNullOnMiss (cache miss returns null), using Mockito to mock the MSAL client.
The reference benchmark set connectRetryCount=0 in the URL but did not mention INTERMITTENT_TLS_MAX_RETRY=0, a hard-coded constant that cannot be set from the connection string. Add a Retry configuration note to the header (and an inline comment) so anyone reproducing the numbers knows to patch that constant in both the baseline and the change build.
@github-project-automation github-project-automation Bot moved this to In progress in MSSQL JDBC Jul 9, 2026
@Divyansh-jain2 Divyansh-jain2 changed the title The Microsoft JDBC Driver for SQL Server currently sends string parameters as fixed maximum lengths, causing significant performance degradation due to poor execution plans. This document analyzes the root cause and proposes solutions to enable precise string parameter sizing for better performance and plan stability. A new defineParameterType() API provides explicit per-parameter control to declare exact string lengths, improving plan quality without extra round-trips. The driver will honor the scaleOrLength argument in setObject(), allowing standard JDBC calls to specify parameter length. A stringParameterDefaultLength connection property implements adaptive high-water-mark sizing, balancing plan cache stability and accuracy by sizing parameters based on observed maximum lengths. Fast Cache Approach - Exploring Possible Optimisations in the Token Acquisition Process Jul 9, 2026
@Divyansh-jain2

Copy link
Copy Markdown
Author

@Divyansh-jain2 please read the following Contributor License Agreement(CLA). If you agree with the CLA, please reply with the following information.

@microsoft-github-policy-service agree [company="{your company}"]

Options:

  • (default - no company specified) I have sole ownership of intellectual property rights to my Submissions and I am not making Submissions in the course of work for my employer.
@microsoft-github-policy-service agree
  • (when company given) I am making Submissions in the course of work for my employer (or my employer has intellectual property rights in my Submissions by contract or applicable law). I have permission from my employer to make Submissions and enter into this Agreement on behalf of my employer. By signing below, the defined term “You” includes me and my employer.
@microsoft-github-policy-service agree company="Microsoft"

Contributor License Agreement

@microsoft-github-policy-service agree company="Microsoft"

@Divyansh-jain2
Divyansh-jain2 marked this pull request as ready for review July 12, 2026 06:55
@machavan machavan changed the title Fast Cache Approach - Exploring Possible Optimisations in the Token Acquisition Process Perf: Fast Cache Approach - Exploring Possible Optimisations in the Token Acquisition Process Jul 13, 2026
@machavan machavan added this to the 13.7.0 milestone Jul 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In progress

Development

Successfully merging this pull request may close these issues.

2 participants