Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 31 additions & 16 deletions README.md

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions scripts/benchmark-request-local.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const results = [
scenario: "tracked Redis hits, shadow ramped out",
useCase: "BenchmarkTrackedRedisShadowRampedOut",
// This exact key's stable shadow sample is about 90.11.
shadowRamp: 50,
shadowPercentage: 50,
}),
await benchmarkDarkShadowDetachment(),
await benchmarkDarkShadowFillDetachment(),
Expand Down Expand Up @@ -314,7 +314,7 @@ async function benchmarkRedisReadDeadlineCoalescing(fanout) {
}
}

async function benchmarkSequentialTrackedRedisHits(iterations, { scenario, useCase, shadowRamp }) {
async function benchmarkSequentialTrackedRedisHits(iterations, { scenario, useCase, shadowPercentage }) {
let redisReadCalls = 0;
let redisWriteCalls = 0;
let redisInvalidationCalls = 0;
Expand Down Expand Up @@ -351,7 +351,7 @@ async function benchmarkSequentialTrackedRedisHits(iterations, { scenario, useCa
defaultConfig: new DialCacheKeyConfig({
ttlSec: { [CacheLayer.REMOTE]: 60 },
ramp: { [CacheLayer.REMOTE]: 100 },
...(shadowRamp === undefined ? {} : { shadowRamp }),
...(shadowPercentage === undefined ? {} : { shadow: { ramp: shadowPercentage } }),
}),
},
);
Expand Down Expand Up @@ -429,7 +429,7 @@ async function benchmarkDarkShadowDetachment() {
defaultConfig: new DialCacheKeyConfig({
ttlSec: { [CacheLayer.REMOTE]: 60 },
ramp: { [CacheLayer.REMOTE]: 0 },
shadowRamp: 100,
shadow: { ramp: 100 },
}),
},
);
Expand Down Expand Up @@ -514,7 +514,7 @@ async function benchmarkDarkShadowFillDetachment() {
defaultConfig: new DialCacheKeyConfig({
ttlSec: { [CacheLayer.REMOTE]: 60 },
ramp: { [CacheLayer.REMOTE]: 0 },
shadowRamp: 100,
shadow: { ramp: 100 },
}),
},
);
Expand Down
25 changes: 21 additions & 4 deletions scripts/test-package.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const rootConsumer = `import {
type RedisWriteRequest,
type Serializer,
type ShadowComparator,
type ShadowConfig,
type ShadowValidationMetricLabels,
type ShadowValidationOutcome,
} from "dialcache";
Expand Down Expand Up @@ -115,7 +116,11 @@ const shadowCacheConfig: DialCacheConfig = {
shadowMaxInFlight: 2,
};
const shadowCache = new DialCache(shadowCacheConfig);
const shadowKeyConfig = new DialCacheKeyConfig({ shadowRamp: 50 });
const shadowConfig: ShadowConfig = {
ramp: 50,
logMismatches: true,
};
const shadowKeyConfig = new DialCacheKeyConfig({ shadow: shadowConfig });
const dogStatsDClient: DatadogDogStatsDClient = {
increment: () => undefined,
histogram: () => undefined,
Expand Down Expand Up @@ -631,7 +636,13 @@ if ("MissingKeyConfigError" in root) {
throw new Error("The removed MissingKeyConfigError class must not be exported from the root ESM entry");
}
const esmDisabledOverlay = root.DialCacheKeyConfig.disabled();
if (esmDisabledOverlay.requestLocal !== false || esmDisabledOverlay.shadowRamp !== 0 || esmDisabledOverlay.ramp[root.CacheLayer.LOCAL] !== 0 || esmDisabledOverlay.ramp[root.CacheLayer.REMOTE] !== 0) {
if (
esmDisabledOverlay.requestLocal !== false
|| esmDisabledOverlay.shadow?.ramp !== 0
|| esmDisabledOverlay.shadow.logMismatches !== false
|| esmDisabledOverlay.ramp[root.CacheLayer.LOCAL] !== 0
|| esmDisabledOverlay.ramp[root.CacheLayer.REMOTE] !== 0
) {
throw new Error("The packed ESM runtime did not build the disabled() kill-switch overlay");
}
let calls = 0;
Expand Down Expand Up @@ -774,7 +785,7 @@ const load = cache.cached(async () => await new Promise(() => undefined), {
defaultConfig: new root.DialCacheKeyConfig({
ttlSec: { [root.CacheLayer.REMOTE]: 60 },
ramp: { [root.CacheLayer.REMOTE]: 100 },
shadowRamp: 100,
shadow: { ramp: 100 },
}),
});
let value = await cache.enable(() => load());
Expand Down Expand Up @@ -863,7 +874,13 @@ if ("MissingKeyConfigError" in root) {
throw new Error("The removed MissingKeyConfigError class must not be exported from the root CommonJS entry");
}
const cjsDisabledOverlay = root.DialCacheKeyConfig.disabled();
if (cjsDisabledOverlay.requestLocal !== false || cjsDisabledOverlay.shadowRamp !== 0 || cjsDisabledOverlay.ramp[root.CacheLayer.LOCAL] !== 0 || cjsDisabledOverlay.ramp[root.CacheLayer.REMOTE] !== 0) {
if (
cjsDisabledOverlay.requestLocal !== false
|| cjsDisabledOverlay.shadow?.ramp !== 0
|| cjsDisabledOverlay.shadow.logMismatches !== false
|| cjsDisabledOverlay.ramp[root.CacheLayer.LOCAL] !== 0
|| cjsDisabledOverlay.ramp[root.CacheLayer.REMOTE] !== 0
) {
throw new Error("The packed CommonJS runtime did not build the disabled() kill-switch overlay");
}
void (async () => {
Expand Down
45 changes: 34 additions & 11 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,23 @@ export enum CacheLayer {
export type Awaitable<T> = T | Promise<T>;
export type LayerConfig = Partial<Record<CacheLayer, number>>;

/** Per-use-case runtime policy for detached Redis shadow work. */
export interface ShadowConfig {
/** Independent stable cohort percentage. Omitted and zero disable shadow work. */
readonly ramp?: number;
/**
* Emit one warning with the logical key and bounded native-JSON strings for
* the compared values for each confirmed mismatch. Defaults to false.
*/
readonly logMismatches?: boolean;
}

export class DialCacheKeyConfig {
/** Per-layer TTLs in seconds, from 1 through 31,536,000 (365 days). */
readonly ttlSec: LayerConfig;
readonly ramp: LayerConfig;
/**
* Percentage of tracked Redis keys that asynchronously exercise Redis
* without changing what serves the caller. Hits validate against the source
* of truth and ramped-down clean misses populate Redis. This shadow cohort is
* independent of the Redis serving ramp. Omitted and zero disable it.
*/
readonly shadowRamp?: number;
/** Per-use-case runtime policy for detached Redis shadow work. */
readonly shadow?: ShadowConfig;
/**
* Memoize successful values for the lifetime of the outermost enabled scope.
* Request-local caching is disabled by default and has no TTL or ramp.
Expand All @@ -36,17 +42,21 @@ export class DialCacheKeyConfig {
constructor(config: {
ttlSec?: LayerConfig;
ramp?: LayerConfig;
shadowRamp?: number;
shadow?: ShadowConfig;
requestLocal?: boolean;
remoteReadTimeoutMs?: number;
}) {
if (config === null || typeof config !== "object" || Array.isArray(config)) {
throw new TypeError("DialCache key config must be an object");
}
if (Object.hasOwn(config, "shadowRamp")) {
throw new TypeError('DialCacheKeyConfig.shadowRamp was replaced by "shadow.ramp"');
}
this.ttlSec = cloneLayerConfig(config.ttlSec, "ttlSec");
this.ramp = cloneLayerConfig(config.ramp, "ramp");
if (config.shadowRamp !== undefined) {
this.shadowRamp = config.shadowRamp;
const shadow = cloneShadowConfig(config.shadow);
if (shadow !== undefined) {
this.shadow = shadow;
}
if (config.requestLocal !== undefined && typeof config.requestLocal !== "boolean") {
throw new TypeError("DialCache requestLocal config must be a boolean");
Expand Down Expand Up @@ -83,7 +93,10 @@ export class DialCacheKeyConfig {
static disabled(): DialCacheKeyConfig {
return new DialCacheKeyConfig({
requestLocal: false,
shadowRamp: 0,
shadow: {
ramp: 0,
logMismatches: false,
},
ramp: {
[CacheLayer.LOCAL]: 0,
[CacheLayer.REMOTE]: 0,
Expand All @@ -102,6 +115,16 @@ function cloneLayerConfig(config: LayerConfig | undefined, name: "ttlSec" | "ram
return { ...config };
}

function cloneShadowConfig(config: ShadowConfig | undefined): ShadowConfig | undefined {
if (config === undefined) {
return undefined;
}
if (config === null || typeof config !== "object" || Array.isArray(config)) {
throw new TypeError("DialCache shadow config must be an object");
}
return { ...config };
}

/**
* Resolves runtime cache policy. Async implementations must settle within a
* finite application-defined deadline; DialCache does not add one.
Expand Down
Loading
Loading