History

Merge pull request #276 from RhysSullivan/rs/secrets-parallel-fanout

perf(secrets): parallelize provider fan-out and header resolution

Rhys Sullivan committed 6 months ago

Commit 36ef8382 parents 370df07 165e7703 files · +100 −51Browse files

Unified · Split

3 files
  1. packages/core/sdk/src/executor.ts +45 −25
  2. packages/plugins/graphql/src/sdk/invoke.ts +28 −11
  3. packages/plugins/openapi/src/sdk/invoke.ts +27 −15
packages/core/sdk/src/executor.ts+45−25
@@ -519,17 +519,20 @@ export const createExecutor = < · 518 unmodified lines
519 return yield* provider.get(id);519 return yield* provider.get(id);
520 }520 }
521 521
522 // Fallback: ask enumerating providers in registration order.522 // Fallback: ask every enumerating provider in parallel. First
523 // First non-null wins. Providers that throw are treated as523 // non-null in registration order wins. Providers that throw
524 // "don't have it" and skipped so one flaky provider doesn't524 // are treated as "don't have it" so one flaky provider can't
525 // block resolution via others.525 // block resolution via others.
526 for (const provider of secretProviders.values()) {526 const candidates = [...secretProviders.values()].filter(
527 if (!provider.list) continue;527 (p) => p.list,
528 const value = yield* provider528 );
529 .get(id)529 const values = yield* Effect.all(
530 .pipe(Effect.catchAll(() => Effect.succeed(null)));530 candidates.map((p) =>
531 if (value !== null) return value;531 p.get(id).pipe(Effect.catchAll(() => Effect.succeed(null))),
532 }532 ),
533 { concurrency: "unbounded" },
534 );
535 for (const value of values) if (value !== null) return value;
533 return null;536 return null;
534 });537 });
535 538
@@ -595,11 +598,17 @@ export const createExecutor = < · 59 unmodified lines
595 598
596 const secretsRemove = (id: string): Effect.Effect<void, Error> =>599 const secretsRemove = (id: string): Effect.Effect<void, Error> =>
597 Effect.gen(function* () {600 Effect.gen(function* () {
598 for (const provider of secretProviders.values()) {601 // Providers don't coordinate on which of them own the id — they
599 if (provider.writable && provider.delete) {602 // each get asked. Most calls are no-ops; fan them out so one
600 yield* provider.delete(id);603 // slow provider doesn't serialize the rest.
601 }604 const deleters = [...secretProviders.values()].filter(
602 }605 (p): p is typeof p & { delete: NonNullable<typeof p.delete> } =>
606 !!(p.writable && p.delete),
607 );
608 yield* Effect.all(
609 deleters.map((p) => p.delete(id)),
610 { concurrency: "unbounded" },
611 );
603 yield* core.delete({612 yield* core.delete({
604 model: "secret",613 model: "secret",
605 where: [{ field: "id", value: id }],614 where: [{ field: "id", value: id }],
@@ -643,15 +652,26 @@ export const createExecutor = < · 37 unmodified lines
643 );652 );
644 }653 }
645 654
646 // Then every provider that can enumerate itself. If a provider655 // Then every provider that can enumerate itself, in parallel.
647 // fails to list (unlocked vault, network error), swallow the656 // If a provider fails to list (unlocked vault, network error),
648 // failure and continue — one flaky provider shouldn't block657 // swallow the failure so one flaky provider can't block the
649 // the whole list.658 // whole list. Merge in registration order afterwards so the
650 for (const [providerKey, provider] of secretProviders.entries()) {659 // "first provider wins" precedence stays deterministic.
651 if (!provider.list) continue;660 const listers = [...secretProviders.entries()].filter(
652 const entries = yield* provider661 ([, p]) => p.list,
653 .list()662 );
654 .pipe(Effect.catchAll(() => Effect.succeed([] as const)));663 const lists = yield* Effect.all(
664 listers.map(([key, p]) =>
665 p
666 .list!()
667 .pipe(
668 Effect.catchAll(() => Effect.succeed([] as const)),
669 Effect.map((entries) => ({ key, entries })),
670 ),
671 ),
672 { concurrency: "unbounded" },
673 );
674 for (const { key, entries } of lists) {
655 for (const entry of entries) {675 for (const entry of entries) {
656 if (byId.has(entry.id)) continue; // core row wins676 if (byId.has(entry.id)) continue; // core row wins
657 byId.set(677 byId.set(
@@ -660,7 +680,7 @@ export const createExecutor = < · 2 unmodified lines
660 id: SecretId.make(entry.id),680 id: SecretId.make(entry.id),
661 scopeId: scope.id,681 scopeId: scope.id,
662 name: entry.name,682 name: entry.name,
663 provider: providerKey,683 provider: key,
664 createdAt: new Date(),684 createdAt: new Date(),
665 }),685 }),
666 );686 );
packages/plugins/graphql/src/sdk/invoke.ts+28−11
@@ -17,18 +17,35 @@ export const resolveHeaders = ( · 16 unmodified lines
17 secrets: { readonly get: (id: string) => Effect.Effect<string | null, Error> },17 secrets: { readonly get: (id: string) => Effect.Effect<string | null, Error> },
18): Effect.Effect<Record<string, string>, Error> =>18): Effect.Effect<Record<string, string>, Error> =>
19 Effect.gen(function* () {19 Effect.gen(function* () {
20 const entries = Object.entries(headers);
21 // Resolve secret-backed headers in parallel. Missing / failing
22 // lookups drop the header rather than fail the invocation, same
23 // as the serial version.
24 const values = yield* Effect.all(
25 entries.map(([name, value]) =>
26 typeof value === "string"
27 ? Effect.succeed<{ readonly name: string; readonly value: string | null }>({
28 name,
29 value,
30 })
31 : secrets.get(value.secretId).pipe(
32 Effect.catchAll(() => Effect.succeed<string | null>(null)),
33 Effect.map((secret) => ({
34 name,
35 value:
36 secret === null
37 ? null
38 : value.prefix
39 ? `${value.prefix}${secret}`
40 : secret,
41 })),
42 ),
43 ),
44 { concurrency: "unbounded" },
45 );
20 const resolved: Record<string, string> = {};46 const resolved: Record<string, string> = {};
21 for (const [name, value] of Object.entries(headers)) {47 for (const { name, value } of values) {
22 if (typeof value === "string") {48 if (value !== null) resolved[name] = value;
23 resolved[name] = value;
24 } else {
25 const secret = yield* secrets.get(value.secretId).pipe(
26 Effect.catchAll(() => Effect.succeed<string | null>(null)),
27 );
28 if (secret !== null) {
29 resolved[name] = value.prefix ? `${value.prefix}${secret}` : secret;
30 }
31 }
32 }49 }
33 return resolved;50 return resolved;
34 });51 });
packages/plugins/openapi/src/sdk/invoke.ts+27−15
@@ -95,22 +95,34 @@ export const resolveHeaders = ( · 94 unmodified lines
95 secrets: { readonly get: (id: string) => Effect.Effect<string | null, Error> },95 secrets: { readonly get: (id: string) => Effect.Effect<string | null, Error> },
96): Effect.Effect<Record<string, string>, Error> =>96): Effect.Effect<Record<string, string>, Error> =>
97 Effect.gen(function* () {97 Effect.gen(function* () {
98 const resolved: Record<string, string> = {};98 const entries = Object.entries(headers);
99 for (const [name, value] of Object.entries(headers)) {99 // Fan out secret lookups: on every invocation, one or two headers
100 if (typeof value === "string") {100 // typically each hit the secret store. Resolving them in parallel
101 resolved[name] = value;101 // is a free wall-clock win — preserved order is only needed for
102 } else {102 // the final assembly, not the fetches.
103 const secret = yield* secrets.get(value.secretId);103 const values = yield* Effect.all(
104 if (secret === null) {104 entries.map(([name, value]) =>
105 return yield* Effect.fail(105 typeof value === "string"
106 new Error(106 ? Effect.succeed({ name, value })
107 `Failed to resolve secret "${value.secretId}" for header "${name}"`,107 : secrets.get(value.secretId).pipe(
108 Effect.flatMap((secret) =>
109 secret === null
110 ? Effect.fail(
111 new Error(
112 `Failed to resolve secret "${value.secretId}" for header "${name}"`,
113 ),
114 )
115 : Effect.succeed({
116 name,
117 value: value.prefix ? `${value.prefix}${secret}` : secret,
118 }),
119 ),
108 ),120 ),
109 );121 ),
110 }122 { concurrency: "unbounded" },
111 resolved[name] = value.prefix ? `${value.prefix}${secret}` : secret;123 );
112 }124 const resolved: Record<string, string> = {};
113 }125 for (const { name, value } of values) resolved[name] = value;
114 return resolved;126 return resolved;
115 });127 });
116 128