Skip to content

Commit c2fdc40

Browse files
authored
Adding max age metrics for schemas (#197)
1 parent ebd5bb6 commit c2fdc40

File tree

10 files changed

+85
-35
lines changed

10 files changed

+85
-35
lines changed

.github/workflows/alpha-release.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ name: Release Alpha
22
run-name: Release Alpha ${{ github.actor }} ${{ github.event_name }}
33

44
on:
5-
schedule:
6-
- cron: '0 8 * * 1,3' # Monday, Wednesday 8am
75
workflow_dispatch:
86
inputs:
97
ref:

src/featureFlag/FeatureFlagProvider.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { readFileSync, writeFileSync } from 'fs';
22
import { join } from 'path';
3-
import axios from 'axios';
3+
import { downloadJson } from '../schema/RemoteSchemaHelper';
44
import { LoggerFactory } from '../telemetry/LoggerFactory';
55
import { Measure } from '../telemetry/TelemetryDecorator';
66
import { Closeable } from '../utils/Closeable';
@@ -58,12 +58,9 @@ export class FeatureFlagProvider implements Closeable {
5858

5959
@Measure({ name: 'getFromOnline' })
6060
private async getFromOnline(env: string): Promise<unknown> {
61-
const response = await axios<unknown>({
62-
method: 'get',
63-
url: `https://raw.githubusercontent.com/aws-cloudformation/cloudformation-languageserver/refs/head/main/assets/featureFlag/${env.toLowerCase()}.json`,
64-
});
65-
66-
return response.data;
61+
return await downloadJson(
62+
`https://raw.githubusercontent.com/aws-cloudformation/cloudformation-languageserver/refs/head/main/assets/featureFlag/${env.toLowerCase()}.json`,
63+
);
6764
}
6865

6966
private log() {

src/schema/GetSamSchemaTask.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,23 @@
11
import { DataStore } from '../datastore/DataStore';
22
import { LoggerFactory } from '../telemetry/LoggerFactory';
33
import { Measure } from '../telemetry/TelemetryDecorator';
4-
import { extractErrorMessage } from '../utils/Errors';
5-
import { downloadFile } from './RemoteSchemaHelper';
6-
import { SamSchemas, SamSchemasType } from './SamSchemas';
4+
import { GetSchemaTask } from './GetSchemaTask';
5+
import { downloadJson } from './RemoteSchemaHelper';
6+
import { SamSchemas, SamSchemasType, SamStoreKey } from './SamSchemas';
77
import { SamSchemaTransformer, SamSchema } from './SamSchemaTransformer';
88

99
const logger = LoggerFactory.getLogger('GetSamSchemaTask');
1010

11-
export class GetSamSchemaTask {
11+
export class GetSamSchemaTask extends GetSchemaTask {
1212
private static readonly SAM_SCHEMA_URL =
1313
'https://raw.githubusercontent.com/aws/serverless-application-model/refs/heads/main/schema_source/sam.schema.json';
1414

15-
@Measure({ name: 'getSamSchema' })
16-
async run(dataStore: DataStore): Promise<void> {
15+
@Measure({ name: 'getSchemas' })
16+
override async runImpl(dataStore: DataStore): Promise<void> {
1717
try {
1818
logger.info('Downloading SAM schema');
1919

20-
const schemaBuffer = await downloadFile(GetSamSchemaTask.SAM_SCHEMA_URL);
21-
const samSchema = JSON.parse(schemaBuffer.toString()) as Record<string, unknown>;
20+
const samSchema = await downloadJson<Record<string, unknown>>(GetSamSchemaTask.SAM_SCHEMA_URL);
2221

2322
const resourceSchemas = SamSchemaTransformer.transformSamSchema(samSchema as unknown as SamSchema);
2423

@@ -36,11 +35,11 @@ export class GetSamSchemaTask {
3635
lastModifiedMs: Date.now(),
3736
};
3837

39-
await dataStore.put('sam-schemas', samSchemasData);
38+
await dataStore.put(SamStoreKey, samSchemasData);
4039

4140
logger.info(`Downloaded and stored ${resourceSchemas.size} SAM resource schemas`);
4241
} catch (error) {
43-
logger.error({ error: extractErrorMessage(error) }, 'Failed to download SAM schema');
42+
logger.error(error, 'Failed to download SAM schema');
4443
throw error;
4544
}
4645
}

src/schema/GetSchemaTask.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { PrivateSchemas, PrivateSchemasType } from './PrivateSchemas';
88
import { RegionalSchemas, RegionalSchemasType, SchemaFileType } from './RegionalSchemas';
99
import { cfnResourceSchemaLink, downloadFile, unZipFile } from './RemoteSchemaHelper';
1010

11-
abstract class GetSchemaTask {
11+
export abstract class GetSchemaTask {
1212
protected abstract runImpl(dataStore: DataStore, logger?: Logger): Promise<void>;
1313

1414
async run(dataStore: DataStore, logger?: Logger) {

src/schema/GetSchemaTaskManager.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ export class GetSchemaTaskManager implements SettingsConfigurable, Closeable {
3636
this.timeout = setTimeout(() => {
3737
// Wait before trying to call CFN APIs so that credentials have time to update
3838
this.runPrivateTask();
39-
void this.runSamTask();
4039
}, TenSeconds);
4140

4241
this.interval = setInterval(() => {
@@ -78,13 +77,11 @@ export class GetSchemaTaskManager implements SettingsConfigurable, Closeable {
7877
.catch(() => {});
7978
}
8079

81-
private async runSamTask(): Promise<void> {
82-
try {
83-
await this.samTask.run(this.schemas.publicSchemas);
84-
this.schemas.invalidateCombinedSchemas();
85-
} catch (error) {
86-
this.log.error({ error }, 'Failed to run SAM schema task');
87-
}
80+
runSamTask() {
81+
this.samTask
82+
.run(this.schemas.samSchemas, this.log)
83+
.then(() => this.schemas.invalidateCombinedSchemas())
84+
.catch(() => {});
8885
}
8986

9087
public currentRegionalTasks() {

src/schema/RemoteSchemaHelper.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,12 @@ export async function unZipFile(buffer: Promise<Buffer>): Promise<SchemaFileType
8282
});
8383
});
8484
}
85+
86+
export async function downloadJson<T = unknown>(url: string): Promise<T> {
87+
const response = await axios<T>({
88+
method: 'get',
89+
url: url,
90+
});
91+
92+
return response.data;
93+
}

src/schema/SamSchemas.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ export type SamSchemasType = {
77
lastModifiedMs: number;
88
};
99

10+
export const SamStoreKey = 'SamSchemas';
11+
1012
export class SamSchemas {
1113
static readonly V1 = 'v1';
1214

src/schema/SchemaRetriever.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ import { DateTime } from 'luxon';
22
import { SettingsConfigurable, ISettingsSubscriber, SettingsSubscription } from '../settings/ISettingsSubscriber';
33
import { DefaultSettings, ProfileSettings } from '../settings/Settings';
44
import { LoggerFactory } from '../telemetry/LoggerFactory';
5+
import { TelemetryService } from '../telemetry/TelemetryService';
56
import { Closeable } from '../utils/Closeable';
67
import { AwsRegion, getRegion } from '../utils/Region';
78
import { CombinedSchemas } from './CombinedSchemas';
89
import { GetSchemaTaskManager } from './GetSchemaTaskManager';
910
import { RegionalSchemasType } from './RegionalSchemas';
11+
import { SamSchemasType, SamStoreKey } from './SamSchemas';
1012
import { SchemaStore } from './SchemaStore';
1113

1214
const StaleDaysThreshold = 7;
@@ -16,11 +18,18 @@ export class SchemaRetriever implements SettingsConfigurable, Closeable {
1618
private settingsSubscription?: SettingsSubscription;
1719
private settings: ProfileSettings = DefaultSettings.profile;
1820
private readonly log = LoggerFactory.getLogger(SchemaRetriever);
21+
private readonly telemetry = TelemetryService.instance.get('SchemaRetriever');
1922

2023
constructor(
2124
private readonly schemaTaskManager: GetSchemaTaskManager,
2225
private readonly schemaStore: SchemaStore,
23-
) {}
26+
) {
27+
this.telemetry.registerGaugeProvider('schema.public.maxAge', () => this.getPublicSchemaMaxAge(), {
28+
unit: 'ms',
29+
});
30+
31+
this.telemetry.registerGaugeProvider('schema.sam.maxAge', () => this.getSamSchemaAge(), { unit: 'ms' });
32+
}
2433

2534
configure(settingsManager: ISettingsSubscriber): void {
2635
// Clean up existing subscription if present
@@ -35,6 +44,7 @@ export class SchemaRetriever implements SettingsConfigurable, Closeable {
3544
// Initialize schemas with current region
3645
this.getRegionalSchemasIfMissing([this.settings.region]);
3746
this.getRegionalSchemasIfStale();
47+
this.getSamSchemasIfMissingOrStale();
3848

3949
// Subscribe to profile settings changes
4050
this.settingsSubscription = settingsManager.subscribe('profile', (newProfileSettings) => {
@@ -125,4 +135,42 @@ export class SchemaRetriever implements SettingsConfigurable, Closeable {
125135
}
126136
}
127137
}
138+
139+
private getSamSchemasIfMissingOrStale() {
140+
const existingValue = this.schemaStore.samSchemas.get<SamSchemasType>(SamStoreKey);
141+
142+
if (existingValue === undefined) {
143+
this.schemaTaskManager.runSamTask();
144+
return;
145+
}
146+
147+
const now = DateTime.now();
148+
const lastModified = DateTime.fromMillis(existingValue.lastModifiedMs);
149+
const isStale = now.diff(lastModified, 'days').days >= StaleDaysThreshold;
150+
151+
if (isStale) {
152+
this.schemaTaskManager.runSamTask();
153+
}
154+
}
155+
156+
private getPublicSchemaMaxAge(): number {
157+
let maxAge = 0;
158+
for (const key of this.schemaStore.publicSchemas.keys(50)) {
159+
const region = getRegion(key);
160+
const existingValue = this.getRegionalSchemasFromStore(region);
161+
if (existingValue) {
162+
const age = DateTime.now().diff(DateTime.fromMillis(existingValue.lastModifiedMs)).milliseconds;
163+
maxAge = Math.max(maxAge, age);
164+
}
165+
}
166+
return maxAge;
167+
}
168+
169+
private getSamSchemaAge(): number {
170+
const existingValue = this.schemaStore.samSchemas.get<SamSchemasType>(SamStoreKey);
171+
if (!existingValue) {
172+
return 0;
173+
}
174+
return DateTime.now().diff(DateTime.fromMillis(existingValue.lastModifiedMs)).milliseconds;
175+
}
128176
}

src/schema/SchemaStore.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { AwsRegion } from '../utils/Region';
33
import { CombinedSchemas } from './CombinedSchemas';
44
import { PrivateSchemasType } from './PrivateSchemas';
55
import { RegionalSchemasType } from './RegionalSchemas';
6-
import { SamSchemasType } from './SamSchemas';
6+
import { SamSchemasType, SamStoreKey } from './SamSchemas';
77

88
export class SchemaStore {
99
public readonly publicSchemas = this.dataStoreFactory.get('public_schemas', Persistence.local);
@@ -20,7 +20,7 @@ export class SchemaStore {
2020
if (!cached) {
2121
const regionalSchemas = this.publicSchemas.get<RegionalSchemasType>(region);
2222
const privateSchemas = this.privateSchemas.get<PrivateSchemasType>(profile);
23-
const samSchemas = this.samSchemas.get<SamSchemasType>('sam-schemas');
23+
const samSchemas = this.samSchemas.get<SamSchemasType>(SamStoreKey);
2424

2525
cached = CombinedSchemas.from(regionalSchemas, privateSchemas, samSchemas);
2626
void this.combinedSchemas.put(cacheKey, cached);

src/services/AwsClient.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ type IamClientConfig = {
99
region: string;
1010
credentials: IamCredentials;
1111
customUserAgent: string;
12-
endpoint?: string;
1312
};
1413

1514
export class AwsClient {
@@ -18,9 +17,11 @@ export class AwsClient {
1817
private readonly cloudformationEndpoint?: string,
1918
) {}
2019

21-
// By default, clients will retry on throttling exceptions 3 times
2220
public getCloudFormationClient() {
23-
return new CloudFormationClient(this.iamClientConfig());
21+
return new CloudFormationClient({
22+
...this.iamClientConfig(),
23+
endpoint: this.cloudformationEndpoint,
24+
});
2425
}
2526

2627
public getCloudControlClient() {
@@ -38,7 +39,6 @@ export class AwsClient {
3839
region: credential.region,
3940
credentials: credential,
4041
customUserAgent: `${ExtensionId}/${ExtensionVersion}`,
41-
endpoint: this.cloudformationEndpoint,
4242
};
4343
} catch {
4444
throw new Error('AWS credentials not configured. Authentication required for online features.');

0 commit comments

Comments
 (0)