|
it("does not rewrite sufficient or persistent watermarks on tracked writes", async () => { |
|
if (client === undefined || admin === undefined) { |
|
throw new Error("Redis test clients did not start"); |
|
} |
|
const scriptClient = client.adapter; |
|
const sufficientValueKey = "write-sufficient:{item:sufficient}:value"; |
|
const sufficientWatermarkKey = "write-sufficient:{item:sufficient}:watermark"; |
|
await admin.set(sufficientWatermarkKey, "1.75", { PX: 120_000 }); |
|
const sufficientTtlBefore = await admin.pTTL(sufficientWatermarkKey); |
|
|
|
expect( |
|
await scriptClient.write({ |
|
valueKey: sufficientValueKey, |
|
watermarkKey: sufficientWatermarkKey, |
|
cacheTtlMs: 2_000, |
|
value: "cached", |
|
}), |
|
).toBe(true); |
|
|
|
expect(await admin.get(sufficientWatermarkKey)).toBe("1.75"); |
|
expect(await admin.pTTL(sufficientWatermarkKey)).toBeGreaterThan(sufficientTtlBefore - 1_000); |
|
expect(await admin.pTTL(sufficientWatermarkKey)).toBeLessThanOrEqual(sufficientTtlBefore); |
|
|
|
const persistentValueKey = "write-persistent:{item:persistent}:value"; |
|
const persistentWatermarkKey = "write-persistent:{item:persistent}:watermark"; |
|
await admin.set(persistentWatermarkKey, "2.25"); |
|
|
|
expect( |
|
await scriptClient.write({ |
|
valueKey: persistentValueKey, |
|
watermarkKey: persistentWatermarkKey, |
|
cacheTtlMs: 2_000, |
|
value: "cached", |
|
}), |
|
).toBe(true); |
|
expect(await admin.get(persistentWatermarkKey)).toBe("2.25"); |
|
expect(await admin.pTTL(persistentWatermarkKey)).toBe(-1); |
|
}); |
|
|
|
it("atomically blocks writes during the buffer and extends watermark TTL", async () => { |
|
if (client === undefined || admin === undefined) { |
|
throw new Error("Redis test clients did not start"); |
|
} |
|
const scriptClient = client.adapter; |
|
const valueKey = "protocol:{item:ttl}:value"; |
|
const watermarkKey = "protocol:{item:ttl}:watermark"; |
|
const writeRequest = { |
|
valueKey, |
|
watermarkKey, |
|
cacheTtlMs: 2_000, |
|
value: "cached", |
|
}; |
|
|
|
expect(await scriptClient.write(writeRequest)).toBe(true); |
|
expect(await admin.get(watermarkKey)).toBe("0"); |
|
const ttlAfterWrite = await admin.pTTL(watermarkKey); |
|
expect(ttlAfterWrite).toBeGreaterThanOrEqual(61_000); |
|
|
|
await scriptClient.invalidate({ watermarkKey, futureBufferMs: 100 }); |
|
expect(await scriptClient.read({ valueKey, watermarkKey })).toBeNull(); |
|
expect(await scriptClient.write({ ...writeRequest, value: "blocked" })).toBe(false); |
|
expect(await scriptClient.read({ valueKey })).toBe("cached"); |
|
const ttlBeforeRead = await admin.pTTL(watermarkKey); |
|
await scriptClient.read({ valueKey, watermarkKey }); |
|
expect(await admin.pTTL(watermarkKey)).toBeLessThanOrEqual(ttlBeforeRead); |
|
|
|
await new Promise((resolve) => setTimeout(resolve, 110)); |
|
expect(await scriptClient.write({ ...writeRequest, value: "fresh" })).toBe(true); |
|
expect(await scriptClient.read({ valueKey, watermarkKey })).toBe("fresh"); |
|
}); |
|
|
|
it("documents that losing a watermark removes its publication fence", async () => { |
|
if (client === undefined || admin === undefined) { |
|
throw new Error("Redis test clients did not start"); |
|
} |
|
const scriptClient = client.adapter; |
|
const valueKey = "watermark-loss:{item:tracked}:value"; |
|
const watermarkKey = "watermark-loss:{item:tracked}:watermark"; |
|
const staleWrite = { |
|
valueKey, |
|
watermarkKey, |
|
cacheTtlMs: 60_000, |
|
value: "stale", |
|
}; |
|
|
|
await scriptClient.invalidate({ watermarkKey, futureBufferMs: 60_000 }); |
|
expect(await scriptClient.write(staleWrite)).toBe(false); |
|
|
|
await admin.del(watermarkKey); |
|
|
|
expect(await scriptClient.write(staleWrite)).toBe(true); |
|
expect(await admin.get(watermarkKey)).toBe("0"); |
Priority
P2 — Medium — measure and document tracked-cache scale limits before high-cardinality or hot-ID adoption; the former fixed-retention P1 risk was removed.
v0.14.0 grooming verification (2026-08-01)
The scale risk remains on main@
34bd022: each tracked identity owns a watermark, all variants share one cluster slot, and tracked node-redis reads remain primary-routed. Thev0.14.0shadow path adds tracked primary reads/fills. Batch invalidation PR #114 improves dispatch but does not remove per-ID hot-slot concentration; proposed stale-on-error #117 would extend value/watermark residency. Keep P2 and gather evidence through #35 before adding controls.Shipped mitigation in v0.10.0
#88 removed the four-hour watermark floor and its public configuration knobs.
Watermark lifetime is now derived from active state:
This removes the stale capacity model that assumed every new tracked ID remained for at least four hours.
Current evidence:
DialCache/src/internal/redis-scripts.ts
Lines 85 to 115 in 84341f2
DialCache/src/internal/redis-scripts.ts
Lines 117 to 156 in 84341f2
DialCache/test/redis-real.integration.test.ts
Lines 629 to 720 in 84341f2
Remaining scale risks
The residual risks are workload-dependent but real:
keyType/idowns one watermark.The relevant capacity model is now approximately:
active distinct tracked-ID rate × effective derived watermark lifetimeplus the workload's ID-skew distribution and tracked-read/write QPS.
Scope
Acceptance criteria