From b3dd82a7ed24e8cf25f6b818084b4fdec7c4654a Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Mon, 28 Nov 2022 16:35:23 -0500 Subject: [PATCH 1/2] Cache API responses for custom value providers --- actions-languageserver/src/index.ts | 5 ++- actions-languageserver/src/on-completion.ts | 24 ++++++++--- actions-languageserver/src/utils/cache.ts | 47 +++++++++++++++++++++ 3 files changed, 70 insertions(+), 6 deletions(-) create mode 100644 actions-languageserver/src/utils/cache.ts diff --git a/actions-languageserver/src/index.ts b/actions-languageserver/src/index.ts index bea1564..8523f5e 100644 --- a/actions-languageserver/src/index.ts +++ b/actions-languageserver/src/index.ts @@ -18,6 +18,7 @@ import { RepositoryContext, } from "./initializationOptions"; import { onCompletion } from "./on-completion"; +import { TTLCache } from "./utils/cache"; // Create a connection for the server, using Node's IPC as a transport. // Also include all preview / proposed LSP features. @@ -28,6 +29,7 @@ const documents: TextDocuments = new TextDocuments(TextDocument); let sessionToken: string | undefined; let repos: RepositoryContext[] = []; +const cache = new TTLCache(); let hasConfigurationCapability = false; let hasWorkspaceFolderCapability = false; @@ -108,7 +110,8 @@ connection.onCompletion( position, documents.get(textDocument.uri)!, sessionToken, - repos.find((repo) => textDocument.uri.startsWith(repo.workspaceUri)) + repos.find((repo) => textDocument.uri.startsWith(repo.workspaceUri)), + cache, ); } ); diff --git a/actions-languageserver/src/on-completion.ts b/actions-languageserver/src/on-completion.ts index 9d314cc..89e45c3 100644 --- a/actions-languageserver/src/on-completion.ts +++ b/actions-languageserver/src/on-completion.ts @@ -1,10 +1,11 @@ import { complete } from "@github/actions-languageservice/complete"; import { WorkflowContext } from "@github/actions-languageservice/context/workflow-context"; -import { ValueProviderConfig } from "@github/actions-languageservice/value-providers/config"; +import { Value, ValueProviderConfig } from "@github/actions-languageservice/value-providers/config"; import { Octokit } from "@octokit/rest"; import { CompletionItem, Position } from "vscode-languageserver"; import { TextDocument } from "vscode-languageserver-textdocument"; import { RepositoryContext } from "./initializationOptions"; +import { TTLCache } from "./utils/cache"; import { getEnvironments } from "./value-providers/job-environment"; import { getRunnerLabels } from "./value-providers/runs-on"; @@ -12,25 +13,38 @@ export async function onCompletion( position: Position, document: TextDocument, sessionToken: string | undefined, - repoContext: RepositoryContext | undefined + repoContext: RepositoryContext | undefined, + cache: TTLCache, ): Promise { const config: ValueProviderConfig = { getCustomValues: async (key: string, context: WorkflowContext) => - getCustomValues(key, context, sessionToken, repoContext), + getCustomValuesWithCache(key, context, sessionToken, repoContext, cache), }; return await complete(document, position, config); } -async function getCustomValues( +async function getCustomValuesWithCache( key: string, context: WorkflowContext, sessionToken: string | undefined, repo: RepositoryContext | undefined, -) { + cache: TTLCache, + ): Promise { if (!sessionToken || !repo) { return; } + const cacheKey = `${repo.owner}/${repo.name}/${key}`; + return cache.get(cacheKey, undefined, async () => await getCustomValues(key, context, sessionToken, repo)); +} + + +async function getCustomValues( + key: string, + _: WorkflowContext, + sessionToken: string, + repo: RepositoryContext, +): Promise { const octokit = new Octokit({ auth: sessionToken, }); diff --git a/actions-languageserver/src/utils/cache.ts b/actions-languageserver/src/utils/cache.ts new file mode 100644 index 0000000..7bd791a --- /dev/null +++ b/actions-languageserver/src/utils/cache.ts @@ -0,0 +1,47 @@ +// From https://github.com/cschleiden/github-actions-parser/blob/a81dec9b7462dbcff08fbad0792f5ad549d9de7d/src/lib/workflowschema/workflowSchema.ts +interface CacheEntry { + cachedAt: number; + content: T; +} + +export class TTLCache { + private cache = new Map>(); + + constructor(private defaultTTLinMS: number = 10 * 60 * 1000) {} + + /** + * + * @param key Key to cache value under + * @param ttlInMS How long is the content valid. If optional, default value will be used + * @param getter Function to retrieve content if not in cache + */ + async get( + key: string, + ttlInMS: number | undefined, + getter: () => Promise + ): Promise { + const hasEntry = this.cache.has(key); + const e = hasEntry && this.cache.get(key); + if ( + hasEntry && + e && + e.cachedAt > Date.now() - (ttlInMS || this.defaultTTLinMS) + ) { + return e.content as T; + } + + try { + const content = await getter(); + + this.cache.set(key, { + cachedAt: Date.now(), + content, + }); + + return content; + } catch (e) { + this.cache.delete(key); + throw e; + } + } +} \ No newline at end of file From 4663319c6f7d8eaa0dd42b0423c1f9fd18c9c921 Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Mon, 28 Nov 2022 18:08:11 -0500 Subject: [PATCH 2/2] Move caching into invidual providers --- actions-languageserver/src/on-completion.ts | 30 +++++++------------ .../src/value-providers/job-environment.ts | 13 +++++++- .../src/value-providers/runs-on.ts | 21 +++++++++++-- 3 files changed, 40 insertions(+), 24 deletions(-) diff --git a/actions-languageserver/src/on-completion.ts b/actions-languageserver/src/on-completion.ts index 89e45c3..7d5ff07 100644 --- a/actions-languageserver/src/on-completion.ts +++ b/actions-languageserver/src/on-completion.ts @@ -18,44 +18,34 @@ export async function onCompletion( ): Promise { const config: ValueProviderConfig = { getCustomValues: async (key: string, context: WorkflowContext) => - getCustomValuesWithCache(key, context, sessionToken, repoContext, cache), + getCustomValues(key, context, sessionToken, repoContext, cache), }; return await complete(document, position, config); } -async function getCustomValuesWithCache( - key: string, - context: WorkflowContext, - sessionToken: string | undefined, - repo: RepositoryContext | undefined, - cache: TTLCache, - ): Promise { - if (!sessionToken || !repo) { - return; - } - - const cacheKey = `${repo.owner}/${repo.name}/${key}`; - return cache.get(cacheKey, undefined, async () => await getCustomValues(key, context, sessionToken, repo)); -} - async function getCustomValues( key: string, _: WorkflowContext, - sessionToken: string, - repo: RepositoryContext, + sessionToken: string | undefined, + repo: RepositoryContext | undefined, + cache: TTLCache, ): Promise { + if (!sessionToken || !repo) { + return; + } + const octokit = new Octokit({ auth: sessionToken, }); switch (key) { case "job-environment": { - return await getEnvironments(octokit, repo.owner, repo.name); + return await getEnvironments(octokit, cache, repo.owner, repo.name); } case "runs-on": { - return await getRunnerLabels(octokit, repo.owner, repo.name); + return await getRunnerLabels(octokit, cache, repo.owner, repo.name); } } } diff --git a/actions-languageserver/src/value-providers/job-environment.ts b/actions-languageserver/src/value-providers/job-environment.ts index 287368e..9f20011 100644 --- a/actions-languageserver/src/value-providers/job-environment.ts +++ b/actions-languageserver/src/value-providers/job-environment.ts @@ -1,11 +1,22 @@ import { Value } from "@github/actions-languageservice/value-providers/config"; import { Octokit } from "@octokit/rest"; +import { TTLCache } from "../utils/cache"; export async function getEnvironments( client: Octokit, + cache: TTLCache, owner: string, name: string ): Promise { + const environments = await cache.get(`${owner}/${name}/environments`, undefined, () => fetchEnvironments(client, owner, name)); + return Array.from(environments).map((env) => ({ label: env })); +} + +async function fetchEnvironments( + client: Octokit, + owner: string, + name: string +): Promise { let environments: string[] = []; try { const response = await client.repos.getAllEnvironments({ @@ -20,5 +31,5 @@ export async function getEnvironments( console.log("Failure to retrieve environments: ", e); } - return Array.from(environments).map((env) => ({ label: env })); + return environments; } diff --git a/actions-languageserver/src/value-providers/runs-on.ts b/actions-languageserver/src/value-providers/runs-on.ts index f4e12a1..4fda670 100644 --- a/actions-languageserver/src/value-providers/runs-on.ts +++ b/actions-languageserver/src/value-providers/runs-on.ts @@ -1,12 +1,14 @@ import { Value } from "@github/actions-languageservice/value-providers/config"; import { Octokit } from "@octokit/rest"; +import { TTLCache } from "../utils/cache"; export async function getRunnerLabels( client: Octokit, + cache: TTLCache, owner: string, name: string ): Promise { - const labels = new Set([ + const defaultLabels = [ "ubuntu-22.04", "ubuntu-latest", "ubuntu-20.04", @@ -20,8 +22,21 @@ export async function getRunnerLabels( "macos-11", "macos-10.15", "self-hosted", - ]); + ]; + const repoLabels = await cache.get(`${owner}/${name}/runner-labels`, undefined, () => fetchRunnerLabels(client, owner, name)); + for (const label of defaultLabels) { + repoLabels.add(label); + } + return Array.from(repoLabels).map((label) => ({ label })); +} + +async function fetchRunnerLabels( + client: Octokit, + owner: string, + name: string +): Promise> { + const labels = new Set(); try { const response = await client.actions.listSelfHostedRunnersForRepo({ owner, @@ -37,5 +52,5 @@ export async function getRunnerLabels( console.log("Failure to retrieve runner labels: ", e); } - return Array.from(labels).map((label) => ({ label })); + return labels; }