@@ -29,14 +29,22 @@ const logger = createLogger('ReportingUsageCache')
2929 *
3030 * A sum is held both in Redis, shared by every process, and in each process that reads it. It
3131 * reflects the ledger as of the moment its sum began, and its Redis expiry is anchored to that
32- * moment, so a sum is served from Redis for at most this TTL plus its jitter after it began —
33- * including however long the sum itself ran — plus up to this TTL again in the reading process.
32+ * moment, so it is served from Redis for at most max( this TTL + jitter, the sum's duration +
33+ * { @link MIN_SHARED_TTL_MS}) after it began, plus up to this TTL again in the reading process.
3434 */
3535export const REPORTING_USAGE_CACHE_TTL_MS = 30_000
3636
3737/** Redis expiry is jittered by up to this much, so payers summed together do not expire together. */
3838const SHARED_TTL_JITTER_MS = 5_000
3939
40+ /**
41+ * The shortest time a sum is kept in Redis. A sum that ran longer than the TTL would otherwise
42+ * expire on arrival, and under the database pressure that makes sums slow, every process would
43+ * then run the same slow sum again. Five seconds shares it with the processes waiting on it while
44+ * adding little staleness next to the time the sum itself took.
45+ */
46+ const MIN_SHARED_TTL_MS = 5_000
47+
4048/**
4149 * How long a read waits on a connected Redis before summing the ledger instead, so a socket that
4250 * has silently stopped answering costs one sum rather than the shared client's long timeouts.
@@ -96,20 +104,26 @@ function warnSharedWriteFailed(error: unknown): void {
96104
97105/**
98106 * Fire-and-forget: a read never waits on, or fails because of, the shared write. The expiry is
99- * anchored to when the sum began (`PXAT`), and a sum already older than the TTL is not written, so
100- * neither a slow sum nor a write the client resends after a reconnect can give an old total a
101- * fresh expiry. The write only lands when no sum is stored (`NX`), so an older sum can never
102- * replace a fresher one or extend its expiry.
107+ * anchored to when the sum began: the remaining lifetime is computed here and written with `PX`,
108+ * which every Redis version accepts, floored at {@link MIN_SHARED_TTL_MS}. A write the client
109+ * resends after a reconnect re-applies that same relative lifetime from the resend, so it can
110+ * extend the expiry by at most the reconnect delay. The write only lands when no sum is stored
111+ * (`NX`), so an older sum can never replace a fresher one or extend its expiry.
103112 */
104113function writeSharedReportingUsageCost ( key : string , cost : number , sumStartedAt : number ) : void {
105114 try {
106- if ( Date . now ( ) >= sumStartedAt + REPORTING_USAGE_CACHE_TTL_MS ) return
107115 const redis = readyRedisClient ( )
108116 if ( ! redis ) return
109- const expiresAt =
110- sumStartedAt + REPORTING_USAGE_CACHE_TTL_MS + randomInt ( 0 , SHARED_TTL_JITTER_MS )
117+ const remainingMs =
118+ sumStartedAt + REPORTING_USAGE_CACHE_TTL_MS + randomInt ( 0 , SHARED_TTL_JITTER_MS ) - Date . now ( )
111119 redis
112- . set ( sharedReportingUsageKey ( key ) , String ( cost ) , 'PXAT' , expiresAt , 'NX' )
120+ . set (
121+ sharedReportingUsageKey ( key ) ,
122+ String ( cost ) ,
123+ 'PX' ,
124+ Math . max ( remainingMs , MIN_SHARED_TTL_MS ) ,
125+ 'NX'
126+ )
113127 . catch ( warnSharedWriteFailed )
114128 } catch ( error ) {
115129 warnSharedWriteFailed ( error )
0 commit comments