perf(secrets): parallelize provider fan-out and header resolution
Unified · Split
| @@ -519,17 +519,20 @@ export const createExecutor = < · 518 unmodified lines | ||
| 519 | 519 | return yield* provider.get(id); |
| 520 | 520 | } |
| 521 | 521 | |
| 522 | − // Fallback: ask enumerating providers in registration order. | |
| 523 | − // First non-null wins. Providers that throw are treated as | |
| 524 | − // "don't have it" and skipped so one flaky provider doesn't | |
| 522 | + // Fallback: ask every enumerating provider in parallel. First | |
| 523 | + // non-null in registration order wins. Providers that throw | |
| 524 | + // are treated as "don't have it" so one flaky provider can't | |
| 525 | 525 | // block resolution via others. |
| 526 | − for (const provider of secretProviders.values()) { | |
| 527 | − if (!provider.list) continue; | |
| 528 | − const value = yield* provider | |
| 529 | − .get(id) | |
| 530 | − .pipe(Effect.catchAll(() => Effect.succeed(null))); | |
| 531 | − if (value !== null) return value; | |
| 532 | − } | |
| 526 | + const candidates = [...secretProviders.values()].filter( | |
| 527 | + (p) => p.list, | |
| 528 | + ); | |
| 529 | + const values = yield* Effect.all( | |
| 530 | + candidates.map((p) => | |
| 531 | + p.get(id).pipe(Effect.catchAll(() => Effect.succeed(null))), | |
| 532 | + ), | |
| 533 | + { concurrency: "unbounded" }, | |
| 534 | + ); | |
| 535 | + for (const value of values) if (value !== null) return value; | |
| 533 | 536 | return null; |
| 534 | 537 | }); |
| 535 | 538 | |
| @@ -595,11 +598,17 @@ export const createExecutor = < · 59 unmodified lines | ||
| 595 | 598 | |
| 596 | 599 | const secretsRemove = (id: string): Effect.Effect<void, Error> => |
| 597 | 600 | Effect.gen(function* () { |
| 598 | − for (const provider of secretProviders.values()) { | |
| 599 | − if (provider.writable && provider.delete) { | |
| 600 | − yield* provider.delete(id); | |
| 601 | − } | |
| 602 | − } | |
| 601 | + // Providers don't coordinate on which of them own the id — they | |
| 602 | + // each get asked. Most calls are no-ops; fan them out so one | |
| 603 | + // slow provider doesn't serialize the rest. | |
| 604 | + const deleters = [...secretProviders.values()].filter( | |
| 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 | 612 | yield* core.delete({ |
| 604 | 613 | model: "secret", |
| 605 | 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 provider | |
| 647 | − // fails to list (unlocked vault, network error), swallow the | |
| 648 | − // failure and continue — one flaky provider shouldn't block | |
| 649 | − // the whole list. | |
| 650 | − for (const [providerKey, provider] of secretProviders.entries()) { | |
| 651 | − if (!provider.list) continue; | |
| 652 | − const entries = yield* provider | |
| 653 | − .list() | |
| 654 | − .pipe(Effect.catchAll(() => Effect.succeed([] as const))); | |
| 655 | + // Then every provider that can enumerate itself, in parallel. | |
| 656 | + // If a provider fails to list (unlocked vault, network error), | |
| 657 | + // swallow the failure so one flaky provider can't block the | |
| 658 | + // whole list. Merge in registration order afterwards so the | |
| 659 | + // "first provider wins" precedence stays deterministic. | |
| 660 | + const listers = [...secretProviders.entries()].filter( | |
| 661 | + ([, p]) => p.list, | |
| 662 | + ); | |
| 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 | 675 | for (const entry of entries) { |
| 656 | 676 | if (byId.has(entry.id)) continue; // core row wins |
| 657 | 677 | byId.set( |
| @@ -660,7 +680,7 @@ export const createExecutor = < · 2 unmodified lines | ||
| 660 | 680 | id: SecretId.make(entry.id), |
| 661 | 681 | scopeId: scope.id, |
| 662 | 682 | name: entry.name, |
| 663 | − provider: providerKey, | |
| 683 | + provider: key, | |
| 664 | 684 | createdAt: new Date(), |
| 665 | 685 | }), |
| 666 | 686 | ); |
| @@ -17,18 +17,35 @@ export const resolveHeaders = ( · 16 unmodified lines | ||
| 17 | 17 | secrets: { readonly get: (id: string) => Effect.Effect<string | null, Error> }, |
| 18 | 18 | ): Effect.Effect<Record<string, string>, Error> => |
| 19 | 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 | 46 | const resolved: Record<string, string> = {}; |
| 21 | − for (const [name, value] of Object.entries(headers)) { | |
| 22 | − if (typeof value === "string") { | |
| 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 | − } | |
| 47 | + for (const { name, value } of values) { | |
| 48 | + if (value !== null) resolved[name] = value; | |
| 32 | 49 | } |
| 33 | 50 | return resolved; |
| 34 | 51 | }); |
| @@ -95,22 +95,34 @@ export const resolveHeaders = ( · 94 unmodified lines | ||
| 95 | 95 | secrets: { readonly get: (id: string) => Effect.Effect<string | null, Error> }, |
| 96 | 96 | ): Effect.Effect<Record<string, string>, Error> => |
| 97 | 97 | Effect.gen(function* () { |
| 98 | − const resolved: Record<string, string> = {}; | |
| 99 | − for (const [name, value] of Object.entries(headers)) { | |
| 100 | − if (typeof value === "string") { | |
| 101 | − resolved[name] = value; | |
| 102 | − } else { | |
| 103 | − const secret = yield* secrets.get(value.secretId); | |
| 104 | − if (secret === null) { | |
| 105 | − return yield* Effect.fail( | |
| 106 | − new Error( | |
| 107 | − `Failed to resolve secret "${value.secretId}" for header "${name}"`, | |
| 98 | + const entries = Object.entries(headers); | |
| 99 | + // Fan out secret lookups: on every invocation, one or two headers | |
| 100 | + // typically each hit the secret store. Resolving them in parallel | |
| 101 | + // is a free wall-clock win — preserved order is only needed for | |
| 102 | + // the final assembly, not the fetches. | |
| 103 | + const values = yield* Effect.all( | |
| 104 | + entries.map(([name, value]) => | |
| 105 | + typeof value === "string" | |
| 106 | + ? Effect.succeed({ name, value }) | |
| 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 | − ); | |
| 110 | − } | |
| 111 | − resolved[name] = value.prefix ? `${value.prefix}${secret}` : secret; | |
| 112 | − } | |
| 113 | − } | |
| 121 | + ), | |
| 122 | + { concurrency: "unbounded" }, | |
| 123 | + ); | |
| 124 | + const resolved: Record<string, string> = {}; | |
| 125 | + for (const { name, value } of values) resolved[name] = value; | |
| 114 | 126 | return resolved; |
| 115 | 127 | }); |
| 116 | 128 | |