11import { assertEquals } from "jsr:@std/assert@^1.0.0" ;
22import { describe , it } from "jsr:@std/testing@^1.0.0/bdd" ;
3+ import { spy } from "jsr:@std/testing@^1.0.0/mock" ;
4+ import { logger } from "./logger.ts" ;
35import { BatchDrainService } from "./batch-drain.ts" ;
46import { createSessionEvent } from "./event-extractor.ts" ;
57import { RedisClient } from "./redis-client.ts" ;
@@ -13,22 +15,69 @@ import {
1315 RedisEventsService ,
1416} from "./redis-events.ts" ;
1517
16- const createDeps = ( ) => {
18+ const createDeps = ( options ?: {
19+ events ?: { claimLockTtlSeconds ?: number } ;
20+ drain ?: { claimHeartbeatIntervalMs ?: number | null } ;
21+ } ) => {
1722 const redis = new RedisClient ( { endpoint : "redis://unused" } ) ;
1823 const events = new RedisEventsService ( redis , {
1924 sessionTtlSeconds : 60 ,
20- claimLockTtlSeconds : 1 ,
25+ claimLockTtlSeconds : options ?. events ?. claimLockTtlSeconds ?? 1 ,
2126 } ) ;
22- const drain = new BatchDrainService ( redis , events , {
27+ const drainOptions = {
2328 batchSize : 2 ,
2429 batchMaxBytes : 20_000 ,
2530 drainRetryMax : 2 ,
26- claimHeartbeatIntervalMs : 100 ,
27- } ) ;
31+ } ;
32+ const heartbeatIntervalMs = options ?. drain ?. claimHeartbeatIntervalMs ;
33+ const drain = new BatchDrainService (
34+ redis ,
35+ events ,
36+ heartbeatIntervalMs === null ? drainOptions : {
37+ ...drainOptions ,
38+ claimHeartbeatIntervalMs : heartbeatIntervalMs ?? 100 ,
39+ } ,
40+ ) ;
2841 return { redis, events, drain } ;
2942} ;
3043
3144describe ( "batch drain" , ( ) => {
45+ it ( "uses a sub-TTL default heartbeat when the claim TTL is small" , ( ) => {
46+ const { drain } = createDeps ( {
47+ events : { claimLockTtlSeconds : 0.2 } ,
48+ drain : { claimHeartbeatIntervalMs : null } ,
49+ } ) ;
50+
51+ const heartbeatIntervalMs = ( drain as unknown as {
52+ getClaimHeartbeatIntervalMs : ( ttl : number ) => number ;
53+ } ) . getClaimHeartbeatIntervalMs ( 1 ) ;
54+
55+ assertEquals ( heartbeatIntervalMs , 333 ) ;
56+ } ) ;
57+
58+ it ( "warns and clamps an explicit heartbeat interval that exceeds the claim TTL budget" , ( ) => {
59+ const warnSpy = spy ( logger , "warn" ) ;
60+ try {
61+ const { drain } = createDeps ( {
62+ events : { claimLockTtlSeconds : 1 } ,
63+ drain : { claimHeartbeatIntervalMs : 1_500 } ,
64+ } ) ;
65+
66+ const heartbeatIntervalMs = ( drain as unknown as {
67+ getClaimHeartbeatIntervalMs : ( ttl : number ) => number ;
68+ } ) . getClaimHeartbeatIntervalMs ( 1 ) ;
69+
70+ assertEquals ( heartbeatIntervalMs , 500 ) ;
71+ assertEquals ( warnSpy . calls . length , 1 ) ;
72+ assertEquals (
73+ warnSpy . calls [ 0 ] . args [ 0 ] ,
74+ "Clamped drain heartbeat interval to stay below claim TTL" ,
75+ ) ;
76+ } finally {
77+ warnSpy . restore ( ) ;
78+ }
79+ } ) ;
80+
3281 it ( "claims oldest events, drains them FIFO, and leaves newer items pending" , async ( ) => {
3382 const { redis, events, drain } = createDeps ( ) ;
3483 const added : string [ ] = [ ] ;
0 commit comments