@@ -4,6 +4,7 @@ import { Logger } from "@trigger.dev/core/logger";
44import type {
55 Prisma ,
66 WebhookDatabase ,
7+ WebhookReplicaDatabase ,
78 WebhookDelivery ,
89 WebhookDeliveryStatus ,
910 WebhookEndpoint ,
@@ -37,7 +38,6 @@ import {
3738import { isInfrastructureError , WebhookInfrastructureError } from "./errors.js" ;
3839import { QueueMemoryGuard } from "./memoryGuard.js" ;
3940import { ensurePartitions , listDatedPartitions , partitionsCoveredUntil } from "./partitions.js" ;
40- import { type CachedEndpoint , TtlCache } from "./cache.js" ;
4141import { evaluateFilter , parseFilter } from "./filter/index.js" ;
4242import { verifyWithSecrets } from "./verification/index.js" ;
4343import { sha256Hex } from "./verification/util.js" ;
@@ -122,6 +122,13 @@ type TargetOutcome =
122122type TaskTarget = Extract < WebhookRoutingTarget , { type : "task" } > ;
123123type SessionTarget = Extract < WebhookRoutingTarget , { type : "session" } > ;
124124
125+ /** An ACTIVE endpoint with its signing secrets (current first) and parsed routing targets. */
126+ type ResolvedEndpoint = {
127+ endpoint : WebhookEndpoint ;
128+ secrets : [ string , ...string [ ] ] ;
129+ targets : ParsedRoutingTarget [ ] ;
130+ } ;
131+
125132/** The response contract declared on a verifier artifact, if any (bundles carry none). */
126133function artifactResponseContract ( artifact : WebhookVerifierArtifact ) {
127134 return "response" in artifact ? artifact . response : undefined ;
@@ -202,7 +209,6 @@ export class WebhookEngine {
202209 private deliveryExecutionDuration : Histogram ;
203210 private deliveryExecutionFailureCounter : Counter ;
204211 private ensurePartitionsCounter : Counter ;
205- private endpointCacheCounter : Counter ;
206212 private ingestCounter : Counter ;
207213 private ingestRefusedCounter : Counter ;
208214 private frontGateRequeuedCounter : Counter ;
@@ -217,19 +223,17 @@ export class WebhookEngine {
217223 private partitionCoverageTimer ?: NodeJS . Timeout ;
218224
219225 prisma : WebhookDatabase ;
226+ private readonly replica : WebhookReplicaDatabase ;
220227
221228 private triggerTask : TriggerWebhookTaskCallback ;
222229 private waiterStore ! : WebhookWaiterStore ;
223230 private readonly waiterLimits : WebhookWaiterLimits ;
224- // Caches the endpoint + resolved signing secret per opaqueId so the ingest hot path skips two
225- // Postgres reads (endpoint lookup + secret decrypt). Both are immutable per endpoint within the
226- // TTL; a status change or secret rotation takes effect after at most the TTL.
227- private endpointCache : TtlCache < CachedEndpoint > ;
228231
229232 constructor ( private readonly options : WebhookEngineOptions ) {
230233 this . logger =
231234 options . logger ?? new Logger ( "WebhookEngine" , ( this . options . logLevel ?? "info" ) as any ) ;
232235 this . prisma = options . prisma ;
236+ this . replica = options . replica ?? options . prisma ;
233237 this . triggerTask = options . triggerTask ;
234238 this . waiterLimits = { ...DEFAULT_WAITER_LIMITS , ...options . waiters ?. limits } ;
235239
@@ -257,9 +261,6 @@ export class WebhookEngine {
257261 this . ensurePartitionsCounter = this . meter . createCounter ( "webhook_ensure_partitions_total" , {
258262 description : "Total number of ensurePartitions cron runs" ,
259263 } ) ;
260- this . endpointCacheCounter = this . meter . createCounter ( "webhook_endpoint_cache_total" , {
261- description : "Endpoint+secret cache lookups on the ingest hot path, by result (hit/miss)" ,
262- } ) ;
263264 this . ingestCounter = this . meter . createCounter ( "webhook_ingest_total" , {
264265 description :
265266 "Webhook requests received, by path (endpoint or a waiter's own URL) and outcome (accepted, duplicate, verification_failed, ...)" ,
@@ -300,11 +301,6 @@ export class WebhookEngine {
300301 result . observe ( ( this . partitionsCoveredUntil . getTime ( ) - Date . now ( ) ) / DAY_MS ) ;
301302 } ) ;
302303
303- this . endpointCache = new TtlCache < CachedEndpoint > (
304- options . endpointCache ?. ttlMs ?? 30_000 ,
305- options . endpointCache ?. maxSize ?? 10_000
306- ) ;
307-
308304 if ( options . disabled ) {
309305 this . logger . info ( "Webhook engine disabled; skipping Redis and worker setup" ) ;
310306 return ;
@@ -385,9 +381,6 @@ export class WebhookEngine {
385381 return startSpan ( this . tracer , "webhook.ingest" , async ( span ) => {
386382 span . setAttribute ( "opaqueId" , input . opaqueId ) ;
387383
388- // 1+2. Resolve the endpoint + signing secret (cached per opaqueId). Fail-closed outcomes
389- // (not found / inactive / secret missing) are never cached, so verify only ever runs with a
390- // non-empty secret.
391384 const resolved = await this . #resolveEndpoint( input . opaqueId ) ;
392385 if ( ! resolved . ok ) return resolved . result ;
393386 const { endpoint, secrets } = resolved ;
@@ -661,7 +654,7 @@ export class WebhookEngine {
661654 this . #assertEnabled( ) ;
662655 return startSpan ( this . tracer , "webhook.unsupportedMethod" , async ( span ) => {
663656 span . setAttribute ( "opaqueId" , opaqueId ) ;
664- const endpoint = await this . prisma . webhookEndpoint . findFirst ( {
657+ const endpoint = await this . replica . webhookEndpoint . findFirst ( {
665658 where : { opaqueId } ,
666659 select : { status : true , verifierArtifact : true } ,
667660 } ) ;
@@ -693,7 +686,7 @@ export class WebhookEngine {
693686 return startSpan ( this . tracer , "webhook.getHandshake" , async ( span ) => {
694687 span . setAttribute ( "opaqueId" , input . opaqueId ) ;
695688
696- const endpoint = await this . prisma . webhookEndpoint . findFirst ( {
689+ const endpoint = await this . replica . webhookEndpoint . findFirst ( {
697690 where : { opaqueId : input . opaqueId } ,
698691 } ) ;
699692 if ( ! endpoint ) return { outcome : "endpoint_not_found" } ;
@@ -1239,20 +1232,11 @@ export class WebhookEngine {
12391232 } ) ;
12401233 }
12411234
1242- // Resolve the endpoint + plaintext signing secret for an opaqueId, cached. Only fully-resolved
1243- // (ACTIVE + secret present) endpoints are cached; fail-closed outcomes always re-read.
12441235 async #resolveEndpoint(
12451236 opaqueId : string
1246- ) : Promise < ( { ok : true } & CachedEndpoint ) | { ok : false ; result : IngestResult } > {
1247- const cached = this . endpointCache . get ( opaqueId ) ;
1248- if ( cached ) {
1249- this . endpointCacheCounter . add ( 1 , { result : "hit" } ) ;
1250- return { ok : true , ...cached } ;
1251- }
1252- this . endpointCacheCounter . add ( 1 , { result : "miss" } ) ;
1253-
1237+ ) : Promise < ( { ok : true } & ResolvedEndpoint ) | { ok : false ; result : IngestResult } > {
12541238 // Single-row via the global @unique on opaqueId (Q2).
1255- const endpoint = await this . prisma . webhookEndpoint . findFirst ( { where : { opaqueId } } ) ;
1239+ const endpoint = await this . replica . webhookEndpoint . findFirst ( { where : { opaqueId } } ) ;
12561240 if ( ! endpoint ) return { ok : false , result : { outcome : "endpoint_not_found" } } ;
12571241 if ( endpoint . status !== "ACTIVE" )
12581242 return { ok : false , result : { outcome : "endpoint_inactive" } } ;
@@ -1277,9 +1261,7 @@ export class WebhookEngine {
12771261
12781262 const targets = parseRoutingTargets ( endpoint , this . logger ) ;
12791263
1280- const secrets : CachedEndpoint [ "secrets" ] = [ secret , ...previousSecrets ] ;
1281- this . endpointCache . set ( opaqueId , { endpoint, secrets, targets } ) ;
1282- return { ok : true , endpoint, secrets, targets } ;
1264+ return { ok : true , endpoint, secrets : [ secret , ...previousSecrets ] , targets } ;
12831265 }
12841266
12851267 // Evaluate a session routing target's `startOn` against the event. Absent => start allowed. A parse or
0 commit comments