Skip to content

Commit 353aa27

Browse files
fix(auth): token refresh rapidly called unexpectedly (#6479)
## Problem: getChatAuthState() is called in many places by the Q features simultaneously, this eventually triggers multiple calls to getToken() and if needed refreshToken(). This resulted in refreshToken being spammed and the Identity team seeing spikes in token refreshes from clients. ## Solution: Throttle getChatAuthState(). Throttling w/ leading: true, allows us to instantly return a fresh result OR a cached result in the case we are throttled. Debounce on the other hand would cause callers to hang since they have to wait for debounce to timeout. Also, we put a debounce on getToken() before in #6282 but this did not work since a new SsoAccessToken instance is created each time the offending code flow triggered (we could look to cache the instance instead which would enable the getToken() debounce to be useful. ### Testing To test the difference after adding the throttle: - Add log statements to `getToken()` - Set an expired date in the SSO cache for both token expiration + client registration expiration - Use chat What would happen is that without throttle it would trigger getChatAuthState() many times, likely due to the connection becoming invalid and sending an event to all Q features, causing each of them to call getChatAuthState() at the same time. But when the throttle was added, the amount of these calls dropped to at most 2. Signed-off-by: nkomonen-amazon <[email protected]> --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license. --------- Signed-off-by: nkomonen-amazon <[email protected]>
1 parent 928593c commit 353aa27

File tree

4 files changed

+18
-30
lines changed

4 files changed

+18
-30
lines changed

packages/amazonq/src/extensionNode.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ async function getAuthState(): Promise<Omit<AuthUserState, 'source'>> {
8282
try {
8383
// May call connection validate functions that try to refresh the token.
8484
// This could result in network errors.
85-
authState = (await AuthUtil.instance.getChatAuthState(false)).codewhispererChat
85+
authState = (await AuthUtil.instance._getChatAuthState(false)).codewhispererChat
8686
} catch (err) {
8787
if (
8888
isNetworkError(err) &&

packages/core/src/auth/sso/ssoAccessTokenProvider.ts

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import { randomUUID } from '../../shared/crypto'
3333
import { getExtRuntimeContext } from '../../shared/vscode/env'
3434
import { showInputBox } from '../../shared/ui/inputPrompter'
3535
import { AmazonQPromptSettings, DevSettings, PromptSettings, ToolkitPromptSettings } from '../../shared/settings'
36-
import { debounce, onceChanged } from '../../shared/utilities/functionUtils'
36+
import { onceChanged } from '../../shared/utilities/functionUtils'
3737
import { NestedMap } from '../../shared/utilities/map'
3838
import { asStringifiedStack } from '../../shared/telemetry/spans'
3939
import { showViewLogsMessage } from '../../shared/utilities/messages'
@@ -97,20 +97,7 @@ export abstract class SsoAccessTokenProvider {
9797
this.reAuthState.set(this.profile, { reAuthReason: `invalidate():${reason}` })
9898
}
9999

100-
/**
101-
* Sometimes we get many calls at once and this
102-
* can trigger redundant disk reads, or token refreshes.
103-
* We debounce to avoid this.
104-
*
105-
* NOTE: The property {@link getTokenDebounced()} does not work with being stubbed for tests, so
106-
* this redundant function was created to work around that.
107-
*/
108100
public async getToken(): Promise<SsoToken | undefined> {
109-
return this.getTokenDebounced()
110-
}
111-
private getTokenDebounced = debounce(() => this._getToken(), 50)
112-
/** Exposed for testing purposes only */
113-
public async _getToken(): Promise<SsoToken | undefined> {
114101
const data = await this.cache.token.load(this.tokenCacheKey)
115102
SsoAccessTokenProvider.logIfChanged(
116103
indent(

packages/core/src/codewhisperer/util/authUtil.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { telemetry } from '../../shared/telemetry/telemetry'
4444
import { asStringifiedStack } from '../../shared/telemetry/spans'
4545
import { withTelemetryContext } from '../../shared/telemetry/util'
4646
import { focusAmazonQPanel } from '../../codewhispererChat/commands/registerCommands'
47+
import { throttle } from 'lodash'
4748

4849
/** Backwards compatibility for connections w pre-chat scopes */
4950
export const codeWhispererCoreScopes = [...scopesCodeWhispererCore]
@@ -402,9 +403,23 @@ export class AuthUtil {
402403
*
403404
* By default, network errors are ignored when determining auth state since they may be silently
404405
* recoverable later.
406+
*
407+
* THROTTLE: This function is called in rapid succession by Amazon Q features and can lead to
408+
* a barrage of disk access and/or token refreshes. We throttle to deal with this.
409+
*
410+
* Note we do an explicit cast of the return type due to Lodash types incorrectly indicating
411+
* a FeatureAuthState or undefined can be returned. But since we set `leading: true`
412+
* it will always return FeatureAuthState
413+
*/
414+
public getChatAuthState = throttle(() => this._getChatAuthState(), 2000, {
415+
leading: true,
416+
}) as () => Promise<FeatureAuthState>
417+
/**
418+
* IMPORTANT: Only use this if you do NOT want to swallow network errors, otherwise use {@link getChatAuthState()}
419+
* @param ignoreNetErr swallows network errors
405420
*/
406421
@withTelemetryContext({ name: 'getChatAuthState', class: authClassName })
407-
public async getChatAuthState(ignoreNetErr: boolean = true): Promise<FeatureAuthState> {
422+
public async _getChatAuthState(ignoreNetErr: boolean = true): Promise<FeatureAuthState> {
408423
// The state of the connection may not have been properly validated
409424
// and the current state we see may be stale, so refresh for latest state.
410425
if (ignoreNetErr) {

packages/core/src/test/credentials/sso/ssoAccessTokenProvider.test.ts

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -156,20 +156,6 @@ describe('SsoAccessTokenProvider', function () {
156156
assert.strictEqual(cachedToken, undefined)
157157
})
158158

159-
it('concurrent calls are debounced', async function () {
160-
const validToken = createToken(hourInMs)
161-
await cache.token.save(startUrl, { region, startUrl, token: validToken })
162-
const actualGetToken = sinon.spy(sut, '_getToken')
163-
164-
const result = await Promise.all([sut.getToken(), sut.getToken(), sut.getToken()])
165-
166-
// Subsequent other calls were debounced so this was only called once
167-
assert.strictEqual(actualGetToken.callCount, 1)
168-
for (const r of result) {
169-
assert.deepStrictEqual(r, validToken)
170-
}
171-
})
172-
173159
describe('Exceptions', function () {
174160
it('drops expired tokens if failure was a client-fault', async function () {
175161
const exception = new UnauthorizedClientException({ message: '', $metadata: {} })

0 commit comments

Comments
 (0)