@@ -20,12 +20,14 @@ describe("schemas", () => {
2020 status : "QUEUED" ,
2121 attempts : "0" ,
2222 createdAt : "2026-05-11T10:00:00.000Z" ,
23+ createdAtMicros : "1747044000000000" ,
2324 } ;
2425 const parsed = BufferEntrySchema . parse ( raw ) ;
2526 expect ( parsed . runId ) . toBe ( "run_abc" ) ;
2627 expect ( parsed . status ) . toBe ( "QUEUED" ) ;
2728 expect ( parsed . attempts ) . toBe ( 0 ) ;
2829 expect ( parsed . createdAt ) . toBeInstanceOf ( Date ) ;
30+ expect ( parsed . createdAtMicros ) . toBe ( 1747044000000000 ) ;
2931 } ) ;
3032
3133 it ( "BufferEntrySchema parses a FAILED entry with lastError" , ( ) => {
@@ -37,6 +39,7 @@ describe("schemas", () => {
3739 status : "FAILED" ,
3840 attempts : "3" ,
3941 createdAt : "2026-05-11T10:00:00.000Z" ,
42+ createdAtMicros : "1747044000000000" ,
4043 lastError : JSON . stringify ( { code : "P2024" , message : "connection lost" } ) ,
4144 } ;
4245 const parsed = BufferEntrySchema . parse ( raw ) ;
@@ -210,7 +213,7 @@ describe("MollifierBuffer.pop orphan handling", () => {
210213
211214 try {
212215 // Simulate a TTL-expired orphan: queue ref exists, entry hash does not.
213- await buffer [ "redis" ] . lpush ( "mollifier:queue:env_a" , "run_orphan" ) ;
216+ await buffer [ "redis" ] . zadd ( "mollifier:queue:env_a" , 1 , "run_orphan" ) ;
214217
215218 const popped = await buffer . pop ( "env_a" ) ;
216219 expect ( popped ) . toBeNull ( ) ;
@@ -220,7 +223,7 @@ describe("MollifierBuffer.pop orphan handling", () => {
220223 expect ( Object . keys ( raw ) ) . toHaveLength ( 0 ) ;
221224
222225 // Queue is drained — the loop pops orphans until empty.
223- const qLen = await buffer [ "redis" ] . llen ( "mollifier:queue:env_a" ) ;
226+ const qLen = await buffer [ "redis" ] . zcard ( "mollifier:queue:env_a" ) ;
224227 expect ( qLen ) . toBe ( 0 ) ;
225228 } finally {
226229 await buffer . close ( ) ;
@@ -243,20 +246,20 @@ describe("MollifierBuffer.pop orphan handling", () => {
243246 } ) ;
244247
245248 try {
246- // Layout (oldest -first, since RPOP takes from tail): orphan, valid, orphan.
247- // LPUSH puts items at the head, so to get RPOP order [orphan_a, valid, orphan_b]
248- // we LPUSH in reverse: orphan_b first, then valid, then orphan_a .
249- await buffer [ "redis" ] . lpush ( "mollifier:queue:env_a" , "orphan_b ") ;
249+ // Layout by score (lowest -first, since ZPOPMIN takes the min):
250+ // orphan_a (score 1) → valid (score = its createdAtMicros, large) → orphan_b (score 1e18).
251+ // First pop skips orphan_a, returns valid; orphan_b remains .
252+ await buffer [ "redis" ] . zadd ( "mollifier:queue:env_a" , 1 , "orphan_a ") ;
250253 await buffer . accept ( { runId : "valid" , envId : "env_a" , orgId : "org_1" , payload : "{}" } ) ;
251- await buffer [ "redis" ] . lpush ( "mollifier:queue:env_a" , "orphan_a ") ;
254+ await buffer [ "redis" ] . zadd ( "mollifier:queue:env_a" , 1e18 , "orphan_b ") ;
252255
253256 const popped = await buffer . pop ( "env_a" ) ;
254257 expect ( popped ) . not . toBeNull ( ) ;
255258 expect ( popped ! . runId ) . toBe ( "valid" ) ;
256259 expect ( popped ! . status ) . toBe ( "DRAINING" ) ;
257260
258261 // The trailing orphan_b is still in the queue (single pop call).
259- const remaining = await buffer [ "redis" ] . llen ( "mollifier:queue:env_a" ) ;
262+ const remaining = await buffer [ "redis" ] . zcard ( "mollifier:queue:env_a" ) ;
260263 expect ( remaining ) . toBe ( 1 ) ;
261264
262265 // A second pop drains the trailing orphan_b. The queue is now
@@ -458,9 +461,13 @@ describe("MollifierBuffer.requeue on missing entry", () => {
458461
459462describe ( "MollifierBuffer.requeue ordering" , ( ) => {
460463 redisTest (
461- "requeued entry is popped AFTER other queued entries on the same env (FIFO retry )" ,
464+ "requeued entry retains its original createdAt and pops next (oldest-first by createdAt )" ,
462465 { timeout : 20_000 } ,
463466 async ( { redisContainer } ) => {
467+ // Score == createdAtMicros; requeue does not bump the score. The
468+ // oldest entry continues to pop first across retries. `maxAttempts`
469+ // in the drainer bounds the retry loop for a persistently failing
470+ // entry (after which it goes to the `fail` path, not requeue).
464471 const buffer = new MollifierBuffer ( {
465472 redisOptions : {
466473 host : redisContainer . getHost ( ) ,
@@ -473,20 +480,23 @@ describe("MollifierBuffer.requeue ordering", () => {
473480
474481 try {
475482 await buffer . accept ( { runId : "a" , envId : "env_a" , orgId : "org_1" , payload : "{}" } ) ;
483+ await new Promise ( ( r ) => setTimeout ( r , 2 ) ) ;
476484 await buffer . accept ( { runId : "b" , envId : "env_a" , orgId : "org_1" , payload : "{}" } ) ;
485+ await new Promise ( ( r ) => setTimeout ( r , 2 ) ) ;
477486 await buffer . accept ( { runId : "c" , envId : "env_a" , orgId : "org_1" , payload : "{}" } ) ;
478487
479488 const first = await buffer . pop ( "env_a" ) ;
480489 expect ( first ! . runId ) . toBe ( "a" ) ;
481490
482491 await buffer . requeue ( "a" ) ;
483492
493+ // a still has the smallest createdAtMicros → pops next.
484494 const next = await buffer . pop ( "env_a" ) ;
485- expect ( next ! . runId ) . toBe ( "b " ) ;
495+ expect ( next ! . runId ) . toBe ( "a " ) ;
486496 const after = await buffer . pop ( "env_a" ) ;
487- expect ( after ! . runId ) . toBe ( "c " ) ;
497+ expect ( after ! . runId ) . toBe ( "b " ) ;
488498 const last = await buffer . pop ( "env_a" ) ;
489- expect ( last ! . runId ) . toBe ( "a " ) ;
499+ expect ( last ! . runId ) . toBe ( "c " ) ;
490500 } finally {
491501 await buffer . close ( ) ;
492502 }
@@ -1026,6 +1036,124 @@ describe("MollifierBuffer envs set lifecycle", () => {
10261036 ) ;
10271037} ) ;
10281038
1039+ describe ( "MollifierBuffer ZSET storage" , ( ) => {
1040+ redisTest (
1041+ "queue key is a ZSET scored by entry's createdAtMicros" ,
1042+ { timeout : 20_000 } ,
1043+ async ( { redisContainer } ) => {
1044+ const buffer = new MollifierBuffer ( {
1045+ redisOptions : {
1046+ host : redisContainer . getHost ( ) ,
1047+ port : redisContainer . getPort ( ) ,
1048+ password : redisContainer . getPassword ( ) ,
1049+ } ,
1050+ entryTtlSeconds : 600 ,
1051+ logger : new Logger ( "test" , "log" ) ,
1052+ } ) ;
1053+
1054+ try {
1055+ await buffer . accept ( { runId : "z1" , envId : "env_z" , orgId : "org_1" , payload : "{}" } ) ;
1056+
1057+ // ZSET-only commands must succeed against the queue key.
1058+ const card = await buffer [ "redis" ] . zcard ( "mollifier:queue:env_z" ) ;
1059+ expect ( card ) . toBe ( 1 ) ;
1060+
1061+ const score = await buffer [ "redis" ] . zscore ( "mollifier:queue:env_z" , "z1" ) ;
1062+ expect ( score ) . not . toBeNull ( ) ;
1063+ const scoreNum = Number ( score ) ;
1064+ expect ( Number . isFinite ( scoreNum ) ) . toBe ( true ) ;
1065+
1066+ // Score matches the entry hash's createdAtMicros field.
1067+ const micros = await buffer [ "redis" ] . hget ( "mollifier:entries:z1" , "createdAtMicros" ) ;
1068+ expect ( micros ) . not . toBeNull ( ) ;
1069+ expect ( Number ( micros ) ) . toBe ( scoreNum ) ;
1070+
1071+ // Score is plausibly recent (within last minute as microseconds).
1072+ const nowMicros = Date . now ( ) * 1000 ;
1073+ expect ( scoreNum ) . toBeGreaterThan ( nowMicros - 60_000_000 ) ;
1074+ expect ( scoreNum ) . toBeLessThanOrEqual ( nowMicros + 1_000_000 ) ;
1075+ } finally {
1076+ await buffer . close ( ) ;
1077+ }
1078+ } ,
1079+ ) ;
1080+
1081+ redisTest (
1082+ "pop returns entries in ascending createdAtMicros order (FIFO by time, not by member)" ,
1083+ { timeout : 20_000 } ,
1084+ async ( { redisContainer } ) => {
1085+ const buffer = new MollifierBuffer ( {
1086+ redisOptions : {
1087+ host : redisContainer . getHost ( ) ,
1088+ port : redisContainer . getPort ( ) ,
1089+ password : redisContainer . getPassword ( ) ,
1090+ } ,
1091+ entryTtlSeconds : 600 ,
1092+ logger : new Logger ( "test" , "log" ) ,
1093+ } ) ;
1094+
1095+ try {
1096+ // Insert runIds in reverse-lex order to prove ordering is by score, not member.
1097+ await buffer . accept ( { runId : "zzz" , envId : "env_o" , orgId : "org_1" , payload : "{}" } ) ;
1098+ await new Promise ( ( r ) => setTimeout ( r , 5 ) ) ;
1099+ await buffer . accept ( { runId : "mmm" , envId : "env_o" , orgId : "org_1" , payload : "{}" } ) ;
1100+ await new Promise ( ( r ) => setTimeout ( r , 5 ) ) ;
1101+ await buffer . accept ( { runId : "aaa" , envId : "env_o" , orgId : "org_1" , payload : "{}" } ) ;
1102+
1103+ const first = await buffer . pop ( "env_o" ) ;
1104+ expect ( first ! . runId ) . toBe ( "zzz" ) ;
1105+ const second = await buffer . pop ( "env_o" ) ;
1106+ expect ( second ! . runId ) . toBe ( "mmm" ) ;
1107+ const third = await buffer . pop ( "env_o" ) ;
1108+ expect ( third ! . runId ) . toBe ( "aaa" ) ;
1109+ } finally {
1110+ await buffer . close ( ) ;
1111+ }
1112+ } ,
1113+ ) ;
1114+
1115+ redisTest (
1116+ "requeue keeps original score; createdAt is immutable across retries" ,
1117+ { timeout : 20_000 } ,
1118+ async ( { redisContainer } ) => {
1119+ const buffer = new MollifierBuffer ( {
1120+ redisOptions : {
1121+ host : redisContainer . getHost ( ) ,
1122+ port : redisContainer . getPort ( ) ,
1123+ password : redisContainer . getPassword ( ) ,
1124+ } ,
1125+ entryTtlSeconds : 600 ,
1126+ logger : new Logger ( "test" , "log" ) ,
1127+ } ) ;
1128+
1129+ try {
1130+ await buffer . accept ( { runId : "rq" , envId : "env_rq" , orgId : "org_1" , payload : "{}" } ) ;
1131+ const originalScore = Number (
1132+ await buffer [ "redis" ] . zscore ( "mollifier:queue:env_rq" , "rq" ) ,
1133+ ) ;
1134+ const originalMicros = Number (
1135+ await buffer [ "redis" ] . hget ( "mollifier:entries:rq" , "createdAtMicros" ) ,
1136+ ) ;
1137+
1138+ await buffer . pop ( "env_rq" ) ;
1139+ await new Promise ( ( r ) => setTimeout ( r , 5 ) ) ;
1140+ await buffer . requeue ( "rq" ) ;
1141+
1142+ const newScore = Number (
1143+ await buffer [ "redis" ] . zscore ( "mollifier:queue:env_rq" , "rq" ) ,
1144+ ) ;
1145+ const newMicros = Number (
1146+ await buffer [ "redis" ] . hget ( "mollifier:entries:rq" , "createdAtMicros" ) ,
1147+ ) ;
1148+ expect ( newScore ) . toBe ( originalScore ) ;
1149+ expect ( newMicros ) . toBe ( originalMicros ) ;
1150+ } finally {
1151+ await buffer . close ( ) ;
1152+ }
1153+ } ,
1154+ ) ;
1155+ } ) ;
1156+
10291157describe ( "MollifierBuffer.listEntriesForEnv" , ( ) => {
10301158 redisTest (
10311159 "returns up to maxCount entries from the queue without consuming them" ,
0 commit comments