Skip to content

Perf: Bound the ActiveDirectoryServicePrincipal token gate with a fixed semaphore pool - #2983

Open
Divyansh-jain2 wants to merge 4 commits into
microsoft:mainfrom
Divyansh-jain2:users/divyansh/pooled-sem
Open

Perf: Bound the ActiveDirectoryServicePrincipal token gate with a fixed semaphore pool#2983
Divyansh-jain2 wants to merge 4 commits into
microsoft:mainfrom
Divyansh-jain2:users/divyansh/pooled-sem

Conversation

@Divyansh-jain2

Copy link
Copy Markdown

Description

On the ActiveDirectoryServicePrincipal token-acquisition path, every call is gated by a single JVM-wide Semaphore(1). That gate protects the Entra ID (AAD) token endpoint from a stampede, but it also serialises token acquisitions for completely unrelated service principals — even though they share no cache state — so unrelated principals block one another under concurrency.

This PR replaces the single global gate (on the SP flow only) with a fixed-size pool of N semaphores (SEM_POOL_SIZE = 10). Each credential is mapped deterministically onto exactly one slot via its hashedSecret:

{stsurl, principalId, secret} --SHA-256--> hashedSecret --hashCode & 0x7fffffff--> % N --> slot
  • Bounded: at most N semaphores exist for the lifetime of the JVM — allocated once in a static initializer, never grown.

  • Deterministic: the same credential always maps to the same slot, so concurrent callers for one principal still serialise on the same permit (preserving the anti-stampede guarantee per credential).

  • Correct under collision: two distinct principals may occasionally share a slot, which costs only a little extra serialisation — never an incorrect token, because token correctness comes entirely from the hashedSecret-keyed TOKEN_CACHE_MAP, not from the gate.

  • API changes / backwards compatibility: None. No public API, connection property, or default behavior change. Only the internal gate for the SP-secret flow changed; the user/password, certificate, integrated, and interactive flows keep the existing global Semaphore(1).

  • Who benefits: Applications opening many concurrent connections across multiple distinct service principals with authentication=ActiveDirectoryServicePrincipal.

How it works

Before:  every SP token acquisition -> [ global Semaphore(1) ] -> aspect/cache -> acquireToken
         principal A blocks principal B blocks principal C ... (all share one permit)

After:   slot = hashedSecret.hashCode() & 0x7fffffff % N
         SP token acquisition -> [ SEM_POOL[slot] ] -> aspect/cache -> acquireToken
         principals only contend if they hash to the same slot (~1/N chance)

The gate partition (hashedSecret → slot) matches the cache partition (hashedSecretTOKEN_CACHE_MAP entry), and TOKEN_SEM_WAIT_DURATION_MS still lets a follower proceed if the slot leader is slow, so a stuck leader never blocks its slot indefinitely.

Why a bounded pool rather than one semaphore per credential: a per-credential map would grow without bound as principals/secrets rotate and is never evicted. The pool caps memory at N semaphores while retaining almost all of the per-principal concurrency benefit.

Testing

Unit tests in SQLServerMSAL4JUtilsTest (offline, no server or Azure credentials):

Test Verifies
testPoolIndexIsAlwaysWithinBounds 10,000 random hashed secrets all map to a slot in [0, SEM_POOL_SIZE) (an out-of-bounds index would throw ArrayIndexOutOfBoundsException on the token path)
testPoolIndexHandlesNegativeHashCode A string with hashCode() == Integer.MIN_VALUE still maps to a non-negative slot — guards the & 0x7fffffff sign-bit mask
testPoolIndexIsDeterministic The same hashed secret always resolves to the same slot, so concurrent callers for one principal serialise on the same semaphore

All existing tests continue to pass.

Performance test methodology

Measured using the driver's built-in PerformanceLogCallback (the TOKEN_ACQUISITION activity from #2706) — no external timing. A reference harness is included at docs/performance/MultiSpTokenAcquisitionTest.java (reference only — not compiled by Maven).

This change only helps a multi-service-principal workload — with a single principal every caller hashes to the same pool slot, so it behaves identically to the global gate. The benchmark therefore round-robins K threads across N distinct principals.

Item Value
Workload K threads, each pinned to one of N distinct service principals (round-robin), looping: open connection → SELECT 1 → close
Auth authentication=ActiveDirectoryServicePrincipal
K (threads) 100
N (distinct SPs) 100
Pool size SEM_POOL_SIZE = 10
Mode warm (tokens pre-cached before the measured window)
Iterations 5 per duration
Retries disabled — connectRetryCount=0 and INTERMITTENT_TLS_MAX_RETRY=0 (both baseline and this branch built identically)
Durations 10s / 60s / 300s / 600s
Target Azure SQL Database
Metric source driver's TOKEN_ACQUISITION via PerformanceLogCallback (ns precision)
Baseline current main (global Semaphore(1)), unmodified apart from the two retry knobs above

Note on retry knobs: connectRetryCount=0 is set via the connection string (no code change). INTERMITTENT_TLS_MAX_RETRY is a hard-coded private final constant, so it was patched to 0 in both builds used for these measurements — the comparison is apples-to-apples, but the absolute numbers come from builds with that constant set to 0, not a stock driver.

Token acquisition avg (ms) — lower is better

Duration Baseline (driver-org) This PR (pooled-sem) Improvement
10s 216.0 119.4 −44.7%
60s 210.7 109.7 −47.9%
300s 221.6 116.1 −47.6%
600s 219.8 117.4 −46.6%

Avg connection latency (ms) — lower is better

Duration Baseline (driver-org) This PR (pooled-sem) Improvement
10s 275.6 220.0 −20.2%
60s 267.6 228.4 −14.6%
300s 281.2 239.2 −14.9%
600s 279.0 236.4 −15.3%

Token acquisition is ~45–48% cheaper across all durations because up to N=10 distinct principals acquire concurrently instead of serialising on one global permit. In the baseline, all 100 principals share one permit, so unrelated token acquisitions queue behind each other (~210–222 ms avg); with the pool they spread across 10 slots (~110–119 ms avg).

Guidelines

Reviewed the contribution guidelines, code of conduct, best practices, coding guidelines, and review process.

Make poolIndexForHashedSecret and SEM_POOL_SIZE package-private so the deterministic credential-to-slot mapping can be unit-tested. Adds SQLServerMSAL4JUtilsTest coverage: the index is always within [0, SEM_POOL_SIZE) (including negative hashCode inputs, guarding the sign-bit mask) and is deterministic for a given hashed secret.
Add a reference-only harness (docs/performance, not compiled by Maven) that drives K threads round-robined across N distinct service principals and reports the driver's TOKEN_ACQUISITION timings via PerformanceLogCallback. This is the multi-SP workload that exercises the cross-principal gate contention the semaphore pool relieves; the single-SP harness shows no difference because all callers hash to one slot.
@github-project-automation github-project-automation Bot moved this to In progress in MSSQL JDBC Jul 9, 2026
@Divyansh-jain2
Divyansh-jain2 marked this pull request as ready for review July 12, 2026 06:56
@machavan machavan changed the title Bound the ActiveDirectoryServicePrincipal token gate with a fixed semaphore pool Perf: Bound the ActiveDirectoryServicePrincipal token gate with a fixed semaphore pool 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