Skip to content

Commit 21af372

Browse files
committed
wip cache resource implementation
1 parent c22efa0 commit 21af372

File tree

5 files changed

+141
-3
lines changed

5 files changed

+141
-3
lines changed

packages/core/src/codewhisperer/region/regionProfileManager.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { isAwsError, ToolkitError } from '../../shared/errors'
2929
import { telemetry } from '../../shared/telemetry/telemetry'
3030
import { localize } from '../../shared/utilities/vsCodeUtils'
3131
import { Commands } from '../../shared/vscode/commands2'
32+
import { CachedResource } from '../../shared/utilities/resourceCache'
3233

3334
// TODO: is there a better way to manage all endpoint strings in one place?
3435
export const defaultServiceConfig: CodeWhispererConfig = {
@@ -59,6 +60,20 @@ export class RegionProfileManager {
5960
// Store the last API results (for UI propuse) so we don't need to call service again if doesn't require "latest" result
6061
private _profiles: RegionProfile[] = []
6162

63+
private readonly cache = new (class extends CachedResource<RegionProfile[]> {
64+
constructor(private readonly profileProvider: () => Promise<RegionProfile[]>) {
65+
super('aws.amazonq.regionProfiles.cache', 60000, {
66+
locked: false,
67+
timestamp: 0,
68+
result: undefined,
69+
})
70+
}
71+
72+
override resourceProvider(): Promise<RegionProfile[]> {
73+
return this.profileProvider()
74+
}
75+
})(this.listRegionProfile.bind(this))
76+
6277
get activeRegionProfile() {
6378
const conn = this.connectionProvider()
6479
if (isBuilderIdConnection(conn)) {
@@ -104,6 +119,14 @@ export class RegionProfileManager {
104119

105120
constructor(private readonly connectionProvider: () => Connection | undefined) {}
106121

122+
async resetCache() {
123+
await this.cache.releaseLock()
124+
}
125+
126+
async getProfiles(): Promise<RegionProfile[]> {
127+
return this.cache.getResource()
128+
}
129+
107130
async listRegionProfile(): Promise<RegionProfile[]> {
108131
this._profiles = []
109132

@@ -238,7 +261,7 @@ export class RegionProfileManager {
238261
return
239262
}
240263
// cross-validation
241-
this.listRegionProfile()
264+
this.getProfiles()
242265
.then(async (profiles) => {
243266
const r = profiles.find((it) => it.arn === previousSelected.arn)
244267
if (!r) {
@@ -300,7 +323,7 @@ export class RegionProfileManager {
300323
const selected = this.activeRegionProfile
301324
let profiles: RegionProfile[] = []
302325
try {
303-
profiles = await this.listRegionProfile()
326+
profiles = await this.getProfiles()
304327
} catch (e) {
305328
return [
306329
{

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ export class AuthUtil {
142142

143143
if (!this.isConnected()) {
144144
await this.regionProfileManager.invalidateProfile(this.regionProfileManager.activeRegionProfile?.arn)
145+
await this.regionProfileManager.resetCache()
145146
}
146147
})
147148

packages/core/src/login/webview/vue/amazonq/backend_amazonq.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ export class AmazonQLoginWebview extends CommonAuthWebview {
223223
*/
224224
override async listRegionProfiles(): Promise<RegionProfile[] | string> {
225225
try {
226-
return await AuthUtil.instance.regionProfileManager.listRegionProfile()
226+
return await AuthUtil.instance.regionProfileManager.getProfiles()
227227
} catch (e) {
228228
const conn = AuthUtil.instance.conn as SsoConnection | undefined
229229
telemetry.amazonq_didSelectProfile.emit({

packages/core/src/shared/globalState.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export type globalKey =
4949
| 'aws.toolkit.lsp.manifest'
5050
| 'aws.amazonq.customization.overrideV2'
5151
| 'aws.amazonq.regionProfiles'
52+
| 'aws.amazonq.regionProfiles.cache'
5253
// Deprecated/legacy names. New keys should start with "aws.".
5354
| '#sessionCreationDates' // Legacy name from `ssoAccessTokenProvider.ts`.
5455
| 'CODECATALYST_RECONNECT'
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import globals from '../extensionGlobals'
7+
import { globalKey } from '../globalState'
8+
import { getLogger } from '../logger/logger'
9+
import { waitUntil } from '../utilities/timeoutUtils'
10+
11+
interface WithLock {
12+
locked: boolean
13+
timestamp: number
14+
}
15+
16+
interface Resource<V> extends WithLock {
17+
result: V | undefined
18+
}
19+
20+
const logger = getLogger()
21+
22+
function now() {
23+
return globals.clock.Date.now()
24+
}
25+
26+
export abstract class CachedResource<V> {
27+
constructor(
28+
readonly key: globalKey,
29+
readonly expiration: number,
30+
private readonly defaultValue: Resource<V>
31+
) {}
32+
33+
abstract resourceProvider(): Promise<V>
34+
35+
async getResource(): Promise<V> {
36+
const resource = await this.readResourceAndLock()
37+
// if cache is still fresh, return
38+
if (resource && resource.result) {
39+
if (now() - resource.timestamp < this.expiration) {
40+
logger.info(`cache hit`)
41+
// release the lock
42+
await globals.globalState.update(this.key, {
43+
...resource,
44+
locked: false,
45+
})
46+
return resource.result
47+
} else {
48+
logger.info(`cache hit but cached value is stale, invoking service API to pull the latest response`)
49+
}
50+
}
51+
52+
// catch and error case?
53+
logger.info(`cache miss, invoking service API to pull the latest response`)
54+
const latest = await this.resourceProvider()
55+
56+
// update resource cache and release the lock
57+
const toUpdate: Resource<V> = {
58+
locked: false,
59+
timestamp: now(),
60+
result: latest,
61+
}
62+
await globals.globalState.update(this.key, toUpdate)
63+
return latest
64+
}
65+
66+
async readResourceAndLock(): Promise<Resource<V> | undefined> {
67+
const _acquireLock = async () => {
68+
const cachedValue = this.readCacheOrDefault()
69+
70+
if (!cachedValue.locked) {
71+
await globals.globalState.update(this.key, {
72+
...cachedValue,
73+
locked: true,
74+
})
75+
76+
return cachedValue
77+
}
78+
79+
return undefined
80+
}
81+
82+
const lock = await waitUntil(
83+
async () => {
84+
const lock = await _acquireLock()
85+
logger.info(`try obtaining cache lock %s`, lock)
86+
if (lock) {
87+
return lock
88+
}
89+
},
90+
{ timeout: 15000, interval: 1500, truthy: true } // TODO: pass via ctor
91+
)
92+
93+
return lock
94+
}
95+
96+
async releaseLock() {
97+
await globals.globalState.update(this.key, {
98+
...this.readCacheOrDefault(),
99+
locked: false,
100+
})
101+
}
102+
103+
private readCacheOrDefault(): Resource<V> {
104+
const cachedValue = globals.globalState.tryGet<Resource<V>>(this.key, Object, {
105+
...this.defaultValue,
106+
locked: false,
107+
result: undefined,
108+
timestamp: 0,
109+
})
110+
111+
return cachedValue
112+
}
113+
}

0 commit comments

Comments
 (0)