Priority
P2 — Medium — numeric-boundary hardening after the current safety foundations.
Current problem
DialCache accepts any positive safe-integer TTL in seconds. The process-local layer multiplies that value by 1,000 without validating the product, while the Redis layer rejects an unsafe millisecond product. futureBufferMs is validated as a nonnegative safe integer before Redis Lua adds it to Redis wall time, but the supported sum is not explicitly bounded.
Current evidence:
- TTL resolution:
|
const ttlSec = config.ttlSec[options.layer]; |
|
if (ttlSec === undefined) { |
|
return { status: "disabled", reason: "missing_config" }; |
|
} |
|
if (!Number.isSafeInteger(ttlSec) || ttlSec <= 0) { |
|
return { status: "disabled", reason: "invalid_ttl" }; |
|
} |
|
|
|
const configuredRamp = config.ramp[options.layer]; |
|
if (configuredRamp === undefined) { |
|
return { status: "disabled", reason: "missing_config" }; |
|
} |
|
if (!Number.isFinite(configuredRamp)) { |
|
return { status: "disabled", reason: "ramped_down" }; |
|
} |
|
|
|
const ramp = clampPercentage(configuredRamp); |
- Process-local multiplication:
|
} |
|
|
|
async put<T>(key: DialCacheKey, value: T, config?: { readonly ttlSec: number }): Promise<void> { |
|
const ttlSec = config?.ttlSec ?? await this.resolveLocalTtlSec(key); |
|
if (ttlSec === null) { |
|
return; |
|
} |
|
|
|
// lru-cache expires when age > ttl, while DialCache historically expired |
|
// when its integer-millisecond clock reached the configured boundary. |
|
const ttlMs = ttlSec * 1000 - 1; |
|
this.cache?.set(key.urn, { value }, { size: 1, ttl: ttlMs }); |
- Redis millisecond validation:
|
try { |
|
const cacheTtlMs = ttlSec * 1000; |
|
if (!Number.isSafeInteger(cacheTtlMs)) { |
|
throw new RangeError("Redis cache TTL is too large"); |
|
} |
- Invalidation input validation and Redis-time addition:
|
function assertValidFutureBufferMs(futureBufferMs: number): void { |
|
if (!Number.isSafeInteger(futureBufferMs) || futureBufferMs < 0) { |
|
throw new RangeError("DialCache invalidation futureBufferMs must be a nonnegative safe integer"); |
|
} |
and
|
export const INVALIDATE_CACHE_SCRIPT = [ |
|
PARSE_WATERMARK_LUA, |
|
CEIL_FINITE_NUMBER_LUA, |
|
String.raw`local future_buffer_ms = ceil_finite_number(ARGV[1]) |
|
local watermark_ttl_floor_ms = ceil_finite_number(ARGV[2]) |
|
if not future_buffer_ms or future_buffer_ms < 0 then |
|
return redis.error_reply("ERR invalid DialCache future buffer") |
|
end |
|
if not watermark_ttl_floor_ms or watermark_ttl_floor_ms <= 0 then |
|
return redis.error_reply("ERR invalid DialCache watermark TTL") |
|
end`, |
|
REDIS_TIME_LUA, |
|
String.raw`local proposed_watermark = now_ms + future_buffer_ms |
|
local raw_watermark = redis.call("GET", KEYS[1]) |
|
local current_watermark = 0 |
|
|
|
if raw_watermark then |
|
local parsed_watermark = parse_watermark(raw_watermark) |
|
if parsed_watermark then |
|
current_watermark = parsed_watermark |
|
end |
|
end |
|
|
|
local watermark = math.ceil(math.max(current_watermark, proposed_watermark)) |
|
local current_ttl_ms = -2 |
|
if raw_watermark then |
|
current_ttl_ms = redis.call("PTTL", KEYS[1]) |
|
end |
|
local desired_ttl_ms = math.max( |
|
watermark_ttl_floor_ms, |
|
future_buffer_ms + ${WATERMARK_TTL_MARGIN_MS}, |
|
watermark - now_ms + ${WATERMARK_TTL_MARGIN_MS} |
|
) |
|
if current_ttl_ms > desired_ttl_ms then |
|
desired_ttl_ms = current_ttl_ms |
|
end |
|
|
|
local encoded_watermark = string.format("%.0f", watermark) |
|
if current_ttl_ms == -1 then |
|
redis.call("SET", KEYS[1], encoded_watermark) |
|
else |
|
redis.call("SET", KEYS[1], encoded_watermark, "PX", desired_ttl_ms) |
|
end`, |
|
"return 1", |
Current implementation dependency
Open #68 adds registration-time validation for static/default TTLs, but still accepts Number.MAX_SAFE_INTEGER seconds. It improves rejection timing without resolving the cross-layer arithmetic bound.
Decisions required before implementation
- Choose the maximum supported TTL in seconds so both local and Redis millisecond representations are safe.
- Choose the maximum supported
futureBufferMs relative to Redis timestamp arithmetic.
- Confirm that invalid static policy is rejected when
cached() registers a definition, while malformed dynamic provider values continue to fail open at invocation time.
Acceptance criteria
- Validate every relevant product and sum before a fallback result reaches storage or invalidation mutates Redis.
- Make process-local and Redis handling consistent for the same effective policy.
- Add exact boundary tests around the selected maxima and
Number.MAX_SAFE_INTEGER.
- Preserve fail-open handling for dynamic policy and explicit invalidation failure reporting.
- Document the supported duration range.
Priority
P2 — Medium — numeric-boundary hardening after the current safety foundations.
Current problem
DialCache accepts any positive safe-integer TTL in seconds. The process-local layer multiplies that value by 1,000 without validating the product, while the Redis layer rejects an unsafe millisecond product.
futureBufferMsis validated as a nonnegative safe integer before Redis Lua adds it to Redis wall time, but the supported sum is not explicitly bounded.Current evidence:
DialCache/src/internal/runtime-config.ts
Lines 39 to 55 in 34a45fa
DialCache/src/internal/local-cache.ts
Lines 92 to 103 in 34a45fa
DialCache/src/internal/redis-cache.ts
Lines 131 to 135 in 34a45fa
DialCache/src/dialcache.ts
Lines 689 to 692 in 34a45fa
DialCache/src/internal/redis-scripts.ts
Lines 121 to 164 in 34a45fa
Current implementation dependency
Open #68 adds registration-time validation for static/default TTLs, but still accepts
Number.MAX_SAFE_INTEGERseconds. It improves rejection timing without resolving the cross-layer arithmetic bound.Decisions required before implementation
futureBufferMsrelative to Redis timestamp arithmetic.cached()registers a definition, while malformed dynamic provider values continue to fail open at invocation time.Acceptance criteria
Number.MAX_SAFE_INTEGER.