@@ -12,7 +12,16 @@ const logger = createLogger('subscription-service')
1212const SWEEP_PAGE_SIZE = 10
1313const SWEEP_MAX_PAGES = 10
1414
15+ type Sweep = 'ended' | 'grace' | 'renewal'
16+
1517export class SubscriptionService implements ISubscriptionService {
18+ /**
19+ * Where each sweep starts. Rows a pass could not handle stay at the front of its query, so the next
20+ * pass starts behind them. Without this, more unhandled rows than one pass reads would hide every
21+ * row after them. It starts over from the front once a pass reaches the end of the rows.
22+ */
23+ private readonly cursors : Record < Sweep , number > = { ended : 0 , grace : 0 , renewal : 0 }
24+
1625 public constructor (
1726 private readonly userSubscriptionRepository : IUserSubscriptionRepository ,
1827 private readonly paymentsService : IPaymentsService ,
@@ -28,14 +37,17 @@ export class SubscriptionService implements ISubscriptionService {
2837 // Ends first, so a subscription that lapsed is not also sent a renewal invoice.
2938 const changed =
3039 ( await this . sweep (
40+ 'ended' ,
3141 ( limit , offset ) => this . userSubscriptionRepository . findPeriodEnded ( now , limit , offset ) ,
3242 ( subscription ) => this . endPeriod ( subscription , currentSettings , now ) ,
3343 ) ) +
3444 ( await this . sweep (
45+ 'grace' ,
3546 ( limit , offset ) => this . userSubscriptionRepository . findGraceEnded ( now , limit , offset ) ,
3647 ( subscription ) => this . expire ( subscription ) ,
3748 ) ) +
3849 ( await this . sweep (
50+ 'renewal' ,
3951 ( limit , offset ) =>
4052 this . userSubscriptionRepository . findDueForRenewal (
4153 new Date ( now . getTime ( ) + RENEWAL_LEAD_SECONDS * 1000 ) ,
@@ -52,14 +64,16 @@ export class SubscriptionService implements ISubscriptionService {
5264 * Each handled subscription leaves the query that found it, so the next page starts where the
5365 * unhandled ones end: skipping them with an offset keeps a subscription that cannot be handled,
5466 * such as one on a plan the operator removed, from blocking the ones behind it. The work of one
55- * pass is bounded.
67+ * pass is bounded, and the next one carries on behind the rows this one skipped .
5668 */
5769 private async sweep (
70+ sweep : Sweep ,
5871 find : ( limit : number , offset : number ) => Promise < UserSubscription [ ] > ,
5972 handle : ( subscription : UserSubscription ) => Promise < boolean > ,
6073 ) : Promise < number > {
6174 let changed = 0
62- let skipped = 0
75+ let skipped = this . cursors [ sweep ]
76+ let reachedEnd = false
6377
6478 for ( let page = 0 ; page < SWEEP_MAX_PAGES ; page ++ ) {
6579 const subscriptions = await find ( SWEEP_PAGE_SIZE , skipped )
@@ -80,10 +94,13 @@ export class SubscriptionService implements ISubscriptionService {
8094 }
8195
8296 if ( subscriptions . length < SWEEP_PAGE_SIZE ) {
97+ reachedEnd = true
8398 break
8499 }
85100 }
86101
102+ this . cursors [ sweep ] = reachedEnd ? 0 : skipped
103+
87104 return changed
88105 }
89106
0 commit comments