Skip to content

Serialize U2M token refreshes across processes - #6759

Open
Vivek1106-04 wants to merge 2 commits into
databricks:mainfrom
Vivek1106-04:fix/token-store-cross-process-lock
Open

Vivek1106-04 wants to merge 2 commits into
databricks:mainfrom
Vivek1106-04:fix/token-store-cross-process-lock

Conversation

@Vivek1106-04

Copy link
Copy Markdown

Changes

Adds cross-process coordination around the U2M read-refresh-write sequence, taking up the TODO in PersistentAuth.refresh.

storage.LockTokenStore takes an advisory lock on ~/.databricks/token-cache.lock (flock on Unix, LockFileEx on Windows) and blocks until it is available. The lock file sits next to token-cache.json and is used by the keyring backend too, which has no file of its own to lock. Once the lock is held, refresh re-reads the store: if the process that held the lock already exchanged the same refresh token, its result is returned instead of exchanging again.

The lock is passed by the two places that build a store shared with other invocations, cmd/auth/token.go and libs/auth/credentials.go. An in-memory store has no other process to coordinate with, so it keeps the previous behavior.

Why

Fixes #6051.

The CLI is stateless, so two invocations for the same profile load the same cached refresh token, both exchange it with the IdP, and race to write the result back. PersistentAuth.refresh documented this as a known gap.

Since the SDKs began appending --force-refresh, the race fails hard rather than silently. ForceRefreshToken has no fallback to the cached token by design, so a losing writer surfaces as forced token refresh: cache update: exit status 45 from the macOS keyring, breaking any automation that runs several CLI invocations at once.

Two decisions worth a reviewer's attention:

  • No timeout on acquiring the lock. The operating system releases an advisory lock when its holder exits, including on a crash, so the lock cannot be left behind by a dead process, and each critical section is a single token exchange bounded by the HTTP client timeout. A timeout would reintroduce a failure mode for the case this change is meant to fix. Happy to add a bounded wait if you would rather have one.
  • --force-refresh semantics are unchanged for late callers. The re-read only short-circuits a process whose token was superseded while it waited. An invocation that starts after a refresh completed still exchanges, so N sequential force-refreshes remain N exchanges. Collapsing those too would mean giving --force-refresh a freshness window, which is a semantic change I did not want to make here.

recoverStoreUpdate is left in place: it still covers a concurrent writer that does not take the lock, such as an older CLI or a direct SDK user.

Tests

  • libs/auth/storage/lock_test.go: lock file creation, release, and contention. The contending holder is a re-executed test binary, because on Unix an flock is shared by every descriptor in the process that took it, so a second in-process acquisition would succeed and prove nothing.
  • libs/auth/u2m/persistent_auth_test.go: a token refreshed by another process while waiting is reused without an exchange (an empty SliceTransport fails the test if one is attempted), an unchanged cache still exchanges and stores, and a lock error is surfaced.
  • acceptance/cmd/auth/token/force-refresh-concurrent: five concurrent auth token --force-refresh invocations for one profile all return a token and leave a valid cache entry. The exchange count is deliberately not asserted there: how many of the five read the cache before the first writer commits is timing-dependent, so it would be flaky. The unit tests pin the dedupe instead.
  • Mutation checks: disabling flock fails the cross-process test; dropping the re-read under the lock fails the reuse test.
  • go test ./libs/auth/... ./cmd/auth/..., go test ./acceptance -run 'TestAccept/cmd/auth', GOOS=windows go build ./..., GOOS=linux go build ./..., ./task fmt, ./task ws, ./task lint (0 issues).
  • ./task test: 10415 tests, 6 failures, all reproducing on a clean main (internal/build fetching from raw.githubusercontent, and libs/dyn/jsonloader asserting a Go version's JSON error text).

The keyring collision itself (exit status 45) cannot be reproduced in the acceptance suite, which has no OS keychain; the plaintext store surfaces the same race as redundant token exchanges instead.

Take an advisory lock on ~/.databricks/token-cache.lock around the
read-refresh-write sequence, and re-read the store once it is held so a
token another process just exchanged is reused instead of exchanged again.
@github-actions

Copy link
Copy Markdown
Contributor

An authorized user can trigger integration tests manually by following the instructions below:

Trigger:
go/deco-tests-run/cli

Inputs:

  • PR number: 6759
  • Commit SHA: 2e6a03eaf7f5ca38f491cd6b5ceaaa45aa3120fe

Checks will be approved automatically on success.

@github-actions

Copy link
Copy Markdown
Contributor

Approval status: pending

/cmd/auth/ - needs approval

Files: cmd/auth/token.go
Suggested: @simonfaltum
Also eligible: @renaudhartert-db, @mihaimitrea-db, @tanmay-db, @hectorcast-db, @parthban-db, @Divyansh-db, @tejaskochar-db, @chrisst, @rauchy

/libs/auth/ - needs approval

8 files changed
Suggested: @simonfaltum
Also eligible: @renaudhartert-db, @mihaimitrea-db, @tanmay-db, @hectorcast-db, @parthban-db, @Divyansh-db, @tejaskochar-db, @chrisst, @rauchy

General files (require maintainer)

5 files changed
Based on git history:

  • @simonfaltum -- recent work in libs/auth/storage/, cmd/auth/, libs/auth/

Any maintainer (@andrewnester, @anton-107, @denik, @pietern, @shreyas-goenka, @simonfaltum, @renaudhartert-db, @janniklasrose, @lennartkats-db, @rugpanov, @rclarey) can approve all areas.
See OWNERS for ownership rules.

1 similar comment
@github-actions

Copy link
Copy Markdown
Contributor

Approval status: pending

/cmd/auth/ - needs approval

Files: cmd/auth/token.go
Suggested: @simonfaltum
Also eligible: @renaudhartert-db, @mihaimitrea-db, @tanmay-db, @hectorcast-db, @parthban-db, @Divyansh-db, @tejaskochar-db, @chrisst, @rauchy

/libs/auth/ - needs approval

8 files changed
Suggested: @simonfaltum
Also eligible: @renaudhartert-db, @mihaimitrea-db, @tanmay-db, @hectorcast-db, @parthban-db, @Divyansh-db, @tejaskochar-db, @chrisst, @rauchy

General files (require maintainer)

5 files changed
Based on git history:

  • @simonfaltum -- recent work in libs/auth/storage/, cmd/auth/, libs/auth/

Any maintainer (@andrewnester, @anton-107, @denik, @pietern, @shreyas-goenka, @simonfaltum, @renaudhartert-db, @janniklasrose, @lennartkats-db, @rugpanov, @rclarey) can approve all areas.
See OWNERS for ownership rules.

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.

Concurrent databricks auth token --force-refresh invocations race on the shared OAuth token cache, causing hard authentication failures

1 participant