1- import {
2- createCache ,
3- DefaultStatefulContext ,
4- MemoryStore ,
5- Namespace ,
6- RedisCacheStore ,
7- UnkeyCache ,
8- } from "@internal/cache" ;
91import type { RedisOptions } from "@internal/redis" ;
2+ import type { BillingCache } from "../billingCache.js" ;
103import { startSpan } from "@internal/tracing" ;
114import { assertExhaustive } from "@trigger.dev/core" ;
125import { DequeuedMessage , RetryOptions } from "@trigger.dev/core/v3" ;
@@ -20,66 +13,25 @@ import { RunEngineOptions } from "../types.js";
2013import { ExecutionSnapshotSystem , getLatestExecutionSnapshot } from "./executionSnapshotSystem.js" ;
2114import { RunAttemptSystem } from "./runAttemptSystem.js" ;
2215import { SystemResources } from "./systems.js" ;
23- import { ServiceValidationError } from "../errors.js" ;
2416
2517export type DequeueSystemOptions = {
2618 resources : SystemResources ;
2719 machines : RunEngineOptions [ "machines" ] ;
2820 executionSnapshotSystem : ExecutionSnapshotSystem ;
2921 runAttemptSystem : RunAttemptSystem ;
30- billing ?: RunEngineOptions [ "billing" ] ;
22+ billingCache : BillingCache ;
3123 redisOptions : RedisOptions ;
3224} ;
3325
34- // Cache TTLs for billing information - shorter than other caches since billing can change
35- const BILLING_FRESH_TTL = 60000 * 5 ; // 5 minutes
36- const BILLING_STALE_TTL = 60000 * 10 ; // 10 minutes
37-
3826export class DequeueSystem {
3927 private readonly $ : SystemResources ;
4028 private readonly executionSnapshotSystem : ExecutionSnapshotSystem ;
4129 private readonly runAttemptSystem : RunAttemptSystem ;
42- private readonly billingCache : UnkeyCache < {
43- billing : { isPaying : boolean } ;
44- } > ;
4530
4631 constructor ( private readonly options : DequeueSystemOptions ) {
4732 this . $ = options . resources ;
4833 this . executionSnapshotSystem = options . executionSnapshotSystem ;
4934 this . runAttemptSystem = options . runAttemptSystem ;
50-
51- // Initialize billing cache
52- const ctx = new DefaultStatefulContext ( ) ;
53- const memory = new MemoryStore ( { persistentMap : new Map ( ) } ) ;
54- const redisCacheStore = new RedisCacheStore ( {
55- name : "dequeue-system" ,
56- connection : {
57- ...options . redisOptions ,
58- keyPrefix : "engine:dequeue-system:cache:" ,
59- } ,
60- useModernCacheKeyBuilder : true ,
61- } ) ;
62-
63- this . billingCache = createCache ( {
64- billing : new Namespace < { isPaying : boolean } > ( ctx , {
65- stores : [ memory , redisCacheStore ] ,
66- fresh : BILLING_FRESH_TTL ,
67- stale : BILLING_STALE_TTL ,
68- } ) ,
69- } ) ;
70- }
71-
72- /**
73- * Invalidates the billing cache for an organization when their plan changes
74- * Runs in background and handles all errors internally
75- */
76- invalidateBillingCache ( orgId : string ) : void {
77- this . billingCache . billing . remove ( orgId ) . catch ( ( error ) => {
78- this . $ . logger . warn ( "Failed to invalidate billing cache" , {
79- orgId,
80- error : error . message ,
81- } ) ;
82- } ) ;
8335 }
8436
8537 /**
@@ -432,8 +384,29 @@ export class DequeueSystem {
432384 const currentAttemptNumber = lockedTaskRun . attemptNumber ?? 0 ;
433385 const nextAttemptNumber = currentAttemptNumber + 1 ;
434386
435- // Get billing information if available
436- const billing = await this . #getBillingInfo( { orgId, runId } ) ;
387+ // Get billing information if available, with fallback to TaskRun.planType
388+ const billingResult = await this . options . billingCache . getCurrentPlan ( orgId ) ;
389+
390+ let isPaying : boolean ;
391+ if ( billingResult . err || ! billingResult . val ) {
392+ // Fallback to stored planType on TaskRun if billing cache fails or returns no value
393+ this . $ . logger . warn (
394+ "Billing cache failed or returned no value, falling back to TaskRun.planType" ,
395+ {
396+ orgId,
397+ runId,
398+ error :
399+ billingResult . err instanceof Error
400+ ? billingResult . err . message
401+ : String ( billingResult . err ) ,
402+ currentPlan : billingResult . val ,
403+ }
404+ ) ;
405+
406+ isPaying = lockedTaskRun . planType !== null && lockedTaskRun . planType !== "free" ;
407+ } else {
408+ isPaying = billingResult . val . isPaying ;
409+ }
437410
438411 const newSnapshot = await this . executionSnapshotSystem . createExecutionSnapshot (
439412 prisma ,
@@ -503,7 +476,11 @@ export class DequeueSystem {
503476 project : {
504477 id : lockedTaskRun . projectId ,
505478 } ,
506- billing,
479+ billing : {
480+ currentPlan : {
481+ isPaying,
482+ } ,
483+ } ,
507484 } satisfies DequeuedMessage ;
508485 }
509486 ) ;
@@ -668,37 +645,4 @@ export class DequeueSystem {
668645 } ) ;
669646 } ) ;
670647 }
671-
672- async #getBillingInfo( {
673- orgId,
674- runId,
675- } : {
676- orgId : string ;
677- runId : string ;
678- } ) : Promise < { currentPlan : { isPaying : boolean } } > {
679- if ( ! this . options . billing ?. getCurrentPlan ) {
680- return { currentPlan : { isPaying : false } } ;
681- }
682-
683- const result = await this . billingCache . billing . swr ( orgId , async ( ) => {
684- // This is safe because options can't change at runtime
685- const planResult = await this . options . billing ! . getCurrentPlan ( orgId ) ;
686-
687- return { isPaying : planResult . isPaying } ;
688- } ) ;
689-
690- if ( result . err ) {
691- throw result . err ;
692- }
693-
694- if ( ! result . val ) {
695- throw new ServiceValidationError (
696- `Could not resolve billing information for organization ${ orgId } ` ,
697- undefined ,
698- { orgId, runId }
699- ) ;
700- }
701-
702- return { currentPlan : { isPaying : result . val . isPaying } } ;
703- }
704648}
0 commit comments