@@ -2,7 +2,7 @@ import { db } from '@sim/db'
22import { account , webhook } from '@sim/db/schema'
33import { createLogger , runWithRequestContext } from '@sim/logger'
44import { toError } from '@sim/utils/errors'
5- import { sleep } from '@sim/utils/helpers'
5+ import { interruptibleSleep } from '@sim/utils/helpers'
66import { generateId } from '@sim/utils/id'
77import { isRecordLike } from '@sim/utils/object'
88import { backoffWithJitter } from '@sim/utils/retry'
@@ -377,11 +377,12 @@ async function requeueWebhookExecutionAfterSetupFailure(
377377 /**
378378 * The database backend executes jobs only through an in-process runner
379379 * and does not apply `delayMs` to it, so the runner sleeps out the
380- * backoff itself; the trigger.dev backend ignores this field and delays
380+ * backoff itself (abort-aware, so cancellation and shutdown don't wait
381+ * out the timer); the trigger.dev backend ignores this field and delays
381382 * server-side.
382383 */
383384 runner : async ( _queuedPayload : unknown , signal : AbortSignal ) => {
384- await sleep ( delayMs )
385+ await interruptibleSleep ( delayMs , signal )
385386 if ( signal . aborted ) return undefined
386387 return executeWebhookJob ( retryPayload , signal )
387388 } ,
@@ -418,6 +419,54 @@ async function requeueWebhookExecutionAfterSetupFailure(
418419 }
419420}
420421
422+ /**
423+ * Restores the terminal failed execution-log row for a setup failure whose
424+ * replacement enqueue failed. Attempts headed for a requeue suppress their
425+ * failure row so the retry can reuse the execution id; once the requeue is
426+ * known to have failed, no retry will run, so the row must be written here or
427+ * the delivery faults without any execution record. Best-effort by design:
428+ * the same infrastructure outage that broke setup may also break this write,
429+ * in which case the faulted run remains the only signal — matching how
430+ * preprocessing's own error logging degrades.
431+ */
432+ async function recordSetupFailureWithoutRequeue (
433+ payload : WebhookExecutionPayload ,
434+ correlation : AsyncExecutionCorrelation ,
435+ error : RetryableSetupError
436+ ) : Promise < void > {
437+ try {
438+ const loggingSession = new LoggingSession (
439+ payload . workflowId ,
440+ correlation . executionId ,
441+ payload . provider ,
442+ correlation . requestId
443+ )
444+ await loggingSession . safeStart ( {
445+ userId : payload . userId ,
446+ workspaceId : payload . workspaceId ,
447+ variables : { } ,
448+ triggerData : { correlation } ,
449+ } )
450+ await loggingSession . safeCompleteWithError ( {
451+ error : {
452+ message : error . message ,
453+ stackTrace : undefined ,
454+ } ,
455+ traceSpans : [ ] ,
456+ skipCost : true ,
457+ } )
458+ } catch ( loggingError ) {
459+ logger . error (
460+ `[${ correlation . requestId } ] Failed to record webhook setup failure after requeue failure` ,
461+ {
462+ workflowId : payload . workflowId ,
463+ executionId : correlation . executionId ,
464+ error : loggingError ,
465+ }
466+ )
467+ }
468+ }
469+
421470export async function executeWebhookJob (
422471 payload : WebhookExecutionPayload ,
423472 externalAbortSignal ?: AbortSignal
@@ -513,23 +562,23 @@ export async function executeWebhookJob(
513562 * A typed setup failure certifies no block ran and the idempotency
514563 * claim was released, so requeueing the same delivery cannot double
515564 * run it; the retry re-admits usage and re-claims from scratch. When
516- * the requeue enqueue itself fails, fall through to the throw so the
517- * run fails loudly rather than dropping the delivery silently.
565+ * the requeue enqueue itself fails, restore the terminal failure row
566+ * the retry-bound attempt suppressed, then fall through to the throw
567+ * so the run fails loudly rather than dropping the delivery silently.
518568 */
519- if (
520- isRetryableSetupError ( error ) &&
521- hasRemainingWebhookInfraRetry ( payload ) &&
522- ( await requeueWebhookExecutionAfterSetupFailure ( payload , correlation , error ) )
523- ) {
524- return {
525- success : false ,
526- requeued : true ,
527- workflowId : payload . workflowId ,
528- executionId,
529- output : { } ,
530- executedAt : new Date ( ) . toISOString ( ) ,
531- provider : payload . provider ,
569+ if ( isRetryableSetupError ( error ) && hasRemainingWebhookInfraRetry ( payload ) ) {
570+ if ( await requeueWebhookExecutionAfterSetupFailure ( payload , correlation , error ) ) {
571+ return {
572+ success : false ,
573+ requeued : true ,
574+ workflowId : payload . workflowId ,
575+ executionId,
576+ output : { } ,
577+ executedAt : new Date ( ) . toISOString ( ) ,
578+ provider : payload . provider ,
579+ }
532580 }
581+ await recordSetupFailureWithoutRequeue ( payload , correlation , error )
533582 }
534583 throw error
535584 }
0 commit comments