From 165e77070bae3cb120deac5b68376a499da7958f Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Fri, 17 Apr 2026 11:56:27 -0700 Subject: [PATCH] perf(secrets): parallelize provider fan-out and header resolution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Several per-request hot paths iterated secret providers with sequential yield*, turning a fan-out into N round-trips. Fix by switching to Effect.all(..., { concurrency: 'unbounded' }). - executor.ts secretsGet fallback: when the core routing-table misses, ask every enumerating provider in parallel; first non-null in registration order wins. Providers that throw are still treated as 'don't have it'. - executor.ts secretsRemove: delete across every writable provider in parallel. Providers don't coordinate on ownership — each gets asked, and most calls are no-ops. - executor.ts secretsList: provider.list() runs in parallel, then the results are merged in registration order so 'first provider wins' precedence remains deterministic. - openapi/invoke.ts + graphql/invoke.ts resolveHeaders: resolve secret-backed headers in parallel. OpenAPI fails the invocation on any missing secret (unchanged semantics); GraphQL drops the header silently on failure (unchanged semantics). N is small (3-5 providers, 1-5 headers) so parallelism is unbounded; no Effect.all concurrency cap needed. --- packages/core/sdk/src/executor.ts | 70 ++++++++++++++-------- packages/plugins/graphql/src/sdk/invoke.ts | 39 ++++++++---- packages/plugins/openapi/src/sdk/invoke.ts | 42 ++++++++----- 3 files changed, 100 insertions(+), 51 deletions(-) diff --git a/packages/core/sdk/src/executor.ts b/packages/core/sdk/src/executor.ts index a8e1d9643..7f184589b 100644 --- a/packages/core/sdk/src/executor.ts +++ b/packages/core/sdk/src/executor.ts @@ -519,17 +519,20 @@ export const createExecutor = < return yield* provider.get(id); } - // Fallback: ask enumerating providers in registration order. - // First non-null wins. Providers that throw are treated as - // "don't have it" and skipped so one flaky provider doesn't + // Fallback: ask every enumerating provider in parallel. First + // non-null in registration order wins. Providers that throw + // are treated as "don't have it" so one flaky provider can't // block resolution via others. - for (const provider of secretProviders.values()) { - if (!provider.list) continue; - const value = yield* provider - .get(id) - .pipe(Effect.catchAll(() => Effect.succeed(null))); - if (value !== null) return value; - } + const candidates = [...secretProviders.values()].filter( + (p) => p.list, + ); + const values = yield* Effect.all( + candidates.map((p) => + p.get(id).pipe(Effect.catchAll(() => Effect.succeed(null))), + ), + { concurrency: "unbounded" }, + ); + for (const value of values) if (value !== null) return value; return null; }); @@ -595,11 +598,17 @@ export const createExecutor = < const secretsRemove = (id: string): Effect.Effect => Effect.gen(function* () { - for (const provider of secretProviders.values()) { - if (provider.writable && provider.delete) { - yield* provider.delete(id); - } - } + // Providers don't coordinate on which of them own the id — they + // each get asked. Most calls are no-ops; fan them out so one + // slow provider doesn't serialize the rest. + const deleters = [...secretProviders.values()].filter( + (p): p is typeof p & { delete: NonNullable } => + !!(p.writable && p.delete), + ); + yield* Effect.all( + deleters.map((p) => p.delete(id)), + { concurrency: "unbounded" }, + ); yield* core.delete({ model: "secret", where: [{ field: "id", value: id }], @@ -643,15 +652,26 @@ export const createExecutor = < ); } - // Then every provider that can enumerate itself. If a provider - // fails to list (unlocked vault, network error), swallow the - // failure and continue — one flaky provider shouldn't block - // the whole list. - for (const [providerKey, provider] of secretProviders.entries()) { - if (!provider.list) continue; - const entries = yield* provider - .list() - .pipe(Effect.catchAll(() => Effect.succeed([] as const))); + // Then every provider that can enumerate itself, in parallel. + // If a provider fails to list (unlocked vault, network error), + // swallow the failure so one flaky provider can't block the + // whole list. Merge in registration order afterwards so the + // "first provider wins" precedence stays deterministic. + const listers = [...secretProviders.entries()].filter( + ([, p]) => p.list, + ); + const lists = yield* Effect.all( + listers.map(([key, p]) => + p + .list!() + .pipe( + Effect.catchAll(() => Effect.succeed([] as const)), + Effect.map((entries) => ({ key, entries })), + ), + ), + { concurrency: "unbounded" }, + ); + for (const { key, entries } of lists) { for (const entry of entries) { if (byId.has(entry.id)) continue; // core row wins byId.set( @@ -660,7 +680,7 @@ export const createExecutor = < id: SecretId.make(entry.id), scopeId: scope.id, name: entry.name, - provider: providerKey, + provider: key, createdAt: new Date(), }), ); diff --git a/packages/plugins/graphql/src/sdk/invoke.ts b/packages/plugins/graphql/src/sdk/invoke.ts index f8340d19a..37190da96 100644 --- a/packages/plugins/graphql/src/sdk/invoke.ts +++ b/packages/plugins/graphql/src/sdk/invoke.ts @@ -17,18 +17,35 @@ export const resolveHeaders = ( secrets: { readonly get: (id: string) => Effect.Effect }, ): Effect.Effect, Error> => Effect.gen(function* () { + const entries = Object.entries(headers); + // Resolve secret-backed headers in parallel. Missing / failing + // lookups drop the header rather than fail the invocation, same + // as the serial version. + const values = yield* Effect.all( + entries.map(([name, value]) => + typeof value === "string" + ? Effect.succeed<{ readonly name: string; readonly value: string | null }>({ + name, + value, + }) + : secrets.get(value.secretId).pipe( + Effect.catchAll(() => Effect.succeed(null)), + Effect.map((secret) => ({ + name, + value: + secret === null + ? null + : value.prefix + ? `${value.prefix}${secret}` + : secret, + })), + ), + ), + { concurrency: "unbounded" }, + ); const resolved: Record = {}; - for (const [name, value] of Object.entries(headers)) { - if (typeof value === "string") { - resolved[name] = value; - } else { - const secret = yield* secrets.get(value.secretId).pipe( - Effect.catchAll(() => Effect.succeed(null)), - ); - if (secret !== null) { - resolved[name] = value.prefix ? `${value.prefix}${secret}` : secret; - } - } + for (const { name, value } of values) { + if (value !== null) resolved[name] = value; } return resolved; }); diff --git a/packages/plugins/openapi/src/sdk/invoke.ts b/packages/plugins/openapi/src/sdk/invoke.ts index 4df1151ea..f0ef583a5 100644 --- a/packages/plugins/openapi/src/sdk/invoke.ts +++ b/packages/plugins/openapi/src/sdk/invoke.ts @@ -95,22 +95,34 @@ export const resolveHeaders = ( secrets: { readonly get: (id: string) => Effect.Effect }, ): Effect.Effect, Error> => Effect.gen(function* () { - const resolved: Record = {}; - for (const [name, value] of Object.entries(headers)) { - if (typeof value === "string") { - resolved[name] = value; - } else { - const secret = yield* secrets.get(value.secretId); - if (secret === null) { - return yield* Effect.fail( - new Error( - `Failed to resolve secret "${value.secretId}" for header "${name}"`, + const entries = Object.entries(headers); + // Fan out secret lookups: on every invocation, one or two headers + // typically each hit the secret store. Resolving them in parallel + // is a free wall-clock win — preserved order is only needed for + // the final assembly, not the fetches. + const values = yield* Effect.all( + entries.map(([name, value]) => + typeof value === "string" + ? Effect.succeed({ name, value }) + : secrets.get(value.secretId).pipe( + Effect.flatMap((secret) => + secret === null + ? Effect.fail( + new Error( + `Failed to resolve secret "${value.secretId}" for header "${name}"`, + ), + ) + : Effect.succeed({ + name, + value: value.prefix ? `${value.prefix}${secret}` : secret, + }), + ), ), - ); - } - resolved[name] = value.prefix ? `${value.prefix}${secret}` : secret; - } - } + ), + { concurrency: "unbounded" }, + ); + const resolved: Record = {}; + for (const { name, value } of values) resolved[name] = value; return resolved; });