Skip to content
Open
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
44 changes: 29 additions & 15 deletions README.md

Large diffs are not rendered by default.

133 changes: 120 additions & 13 deletions scripts/test-package.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ const rootConsumer = `import {
FallbackTimeoutError,
JsonSerializer,
RedisReadTimeoutError,
type CacheMissReason,
type CacheMetricLabels,
type CacheConfigProvider,
type CachedOptions,
Expand All @@ -145,10 +146,12 @@ const rootConsumer = `import {
type InvalidationMetricLabels,
type MetricErrorKind,
type MetricLayer,
type MissMetricLabels,
type ProcessCoalescingState,
type RedisConfig,
type RedisInvalidationRequest,
type RedisReadContext,
type RedisReadMiss,
type RedisReadResult,
type RedisWatermarkMiss,
type RedisWriteRequest,
Expand All @@ -173,6 +176,7 @@ import type { DialCacheNodeRedisScripts } from "dialcache/node-redis";
import {
ceilSupportedCacheTtlMs,
decodeRedisFrame,
decodeRedisReadResult,
decodeTrackedRedisFrame,
decodeTrackedRedisReadResult,
encodeRedisFrame,
Expand Down Expand Up @@ -220,7 +224,10 @@ const inlineOptionsFor = (useCase: string, key = "1") => ({
});
const metrics: DialCacheMetricsAdapter = {
request: () => undefined,
miss: () => undefined,
miss: (labels: MissMetricLabels) => {
const reason: CacheMissReason = labels.reason;
void reason;
},
disabled: () => undefined,
error: () => undefined,
invalidation: () => undefined,
Expand Down Expand Up @@ -339,6 +346,30 @@ const emptyRedisFrame = Buffer.alloc(10);
emptyRedisFrame[0] = 1;
emptyRedisFrame.writeBigUInt64BE(1n, 1);
const decodedEmptyRedisFrame: DecodedRedisFrame | null = decodeRedisFrame(emptyRedisFrame);
const decodedRedisReadResult: RedisReadResult = decodeRedisReadResult(null);
const explicitlyUndefinedReadMiss: RedisReadMiss = {
reason: "value_absent",
payload: undefined,
createdAtMs: undefined,
};
const explicitlyUndefinedWatermarkMiss: RedisWatermarkMiss = {
kind: "watermark_miss",
reason: "watermark_fenced",
observedWatermarkMs: 1,
payload: undefined,
createdAtMs: undefined,
};
void explicitlyUndefinedReadMiss;
void explicitlyUndefinedWatermarkMiss;
if (
decodedRedisReadResult !== null
&& "reason" in decodedRedisReadResult
&& !("kind" in decodedRedisReadResult)
) {
const typedRedisReadMiss: RedisReadMiss = decodedRedisReadResult;
const reason: CacheMissReason = typedRedisReadMiss.reason;
void reason;
}
const decodedStaleRedisFrame: DecodedRedisFrame | null = decodeTrackedRedisFrame(
emptyRedisFrame,
Buffer.from("1"),
Expand Down Expand Up @@ -503,6 +534,24 @@ const cacheMetricLabels: CacheMetricLabels = {
keyType: "id",
layer: CacheLayer.LOCAL,
};
const missMetricLabels: MissMetricLabels = {
...cacheMetricLabels,
reason: "value_absent",
};
const missReasons: Readonly<Record<CacheMissReason, true>> = {
value_absent: true,
watermark_fenced: true,
unclassified: true,
};
// @ts-expect-error Miss reasons are a bounded public taxonomy.
const unboundedMissReason: CacheMissReason = "expired";
// @ts-expect-error The reason is required only for the miss callback's labels.
const missingMissReason: MissMetricLabels = cacheMetricLabels;
const cacheMetricLabelsWithMissReason: CacheMetricLabels = {
...cacheMetricLabels,
// @ts-expect-error CacheMetricLabels intentionally remains shared by non-miss callbacks.
reason: "value_absent",
};
const invalidationMetricLabels: InvalidationMetricLabels = {
cacheNamespace: "consumer-cache",
keyType: "id",
Expand Down Expand Up @@ -704,6 +753,11 @@ void staleKeyConfig;
void staleRecoveryMaxAgeSec;
void requestLocalCoalescingLabels;
void cacheMetricLabels;
void missMetricLabels;
void missReasons;
void unboundedMissReason;
void missingMissReason;
void cacheMetricLabelsWithMissReason;
void invalidationMetricLabels;
void keyInitHasNoUrnPrefix;
void legacyKeyInit;
Expand Down Expand Up @@ -1004,25 +1058,51 @@ const esmRoundTrip = redisProtocol.decodeRedisFrame(redisProtocol.encodeRedisFra
if (esmRoundTrip?.payload !== "value" || esmRoundTrip.createdAtMs !== 1) {
throw new Error("The packed ESM Redis protocol encoder did not round-trip through the decoder");
}
const esmAbsentRead = redisProtocol.decodeRedisReadResult(null);
if (
esmAbsentRead?.reason !== "value_absent"
|| "kind" in esmAbsentRead
|| "payload" in esmAbsentRead
) {
throw new Error("The packed ESM Redis result decoder did not classify an absent value");
}
if (redisProtocol.decodeTrackedRedisFrame(redisProtocol.encodeRedisFrame("pending", 0), Buffer.from("0")) !== null) {
throw new Error("The packed ESM tracked decoder did not fence an equal timestamp");
}
const esmWatermarkMiss = redisProtocol.decodeTrackedRedisReadResult(
redisProtocol.encodeRedisFrame("pending", 0),
Buffer.from("0"),
redisProtocol.encodeRedisFrame("pending", 1),
Buffer.from("1"),
);
if (
esmWatermarkMiss?.kind !== "watermark_miss"
|| esmWatermarkMiss.observedWatermarkMs !== 0
|| esmWatermarkMiss.reason !== "watermark_fenced"
|| esmWatermarkMiss.observedWatermarkMs !== 1
|| "payload" in esmWatermarkMiss
|| "createdAtMs" in esmWatermarkMiss
) {
throw new Error("The packed ESM tracked result decoder did not preserve the observed watermark miss");
throw new Error("The packed ESM tracked result decoder did not classify a watermark-fenced miss");
}
if (redisProtocol.decodeTrackedRedisFrame(redisProtocol.encodeRedisFrame("value", 1), null)?.payload !== "value") {
throw new Error("The packed ESM tracked decoder did not use zero for a missing watermark");
}
if (redisProtocol.decodeTrackedRedisReadResult(null, null) !== null) {
throw new Error("The packed ESM tracked result decoder did not preserve a generic miss without a watermark");
const esmAbsentTrackedRead = redisProtocol.decodeTrackedRedisReadResult(null, null);
if (
esmAbsentTrackedRead?.reason !== "value_absent"
|| "observedWatermarkMs" in esmAbsentTrackedRead
|| "kind" in esmAbsentTrackedRead
|| "payload" in esmAbsentTrackedRead
) {
throw new Error("The packed ESM tracked result decoder did not classify an absent value");
}
const esmAbsentTrackedReadWithWatermark = redisProtocol.decodeTrackedRedisReadResult(null, Buffer.from("7"));
if (
esmAbsentTrackedReadWithWatermark?.kind !== "watermark_miss"
|| esmAbsentTrackedReadWithWatermark.reason !== "value_absent"
|| esmAbsentTrackedReadWithWatermark.observedWatermarkMs !== 7
|| "payload" in esmAbsentTrackedReadWithWatermark
|| "createdAtMs" in esmAbsentTrackedReadWithWatermark
) {
throw new Error("The packed ESM tracked result decoder did not preserve an absent-value refill fence");
}
if (
"REDIS_FRAME_VERSION" in redisProtocol
Expand Down Expand Up @@ -1393,25 +1473,51 @@ const cjsRoundTrip = redisProtocol.decodeRedisFrame(redisProtocol.encodeRedisFra
if (cjsRoundTrip?.payload !== "value" || cjsRoundTrip.createdAtMs !== 1) {
throw new Error("The packed CommonJS Redis protocol encoder did not round-trip through the decoder");
}
const cjsAbsentRead = redisProtocol.decodeRedisReadResult(null);
if (
cjsAbsentRead?.reason !== "value_absent"
|| "kind" in cjsAbsentRead
|| "payload" in cjsAbsentRead
) {
throw new Error("The packed CommonJS Redis result decoder did not classify an absent value");
}
if (redisProtocol.decodeTrackedRedisFrame(redisProtocol.encodeRedisFrame("pending", 0), Buffer.from("0")) !== null) {
throw new Error("The packed CommonJS tracked decoder did not fence an equal timestamp");
}
const cjsWatermarkMiss = redisProtocol.decodeTrackedRedisReadResult(
redisProtocol.encodeRedisFrame("pending", 0),
Buffer.from("0"),
redisProtocol.encodeRedisFrame("pending", 1),
Buffer.from("1"),
);
if (
cjsWatermarkMiss?.kind !== "watermark_miss"
|| cjsWatermarkMiss.observedWatermarkMs !== 0
|| cjsWatermarkMiss.reason !== "watermark_fenced"
|| cjsWatermarkMiss.observedWatermarkMs !== 1
|| "payload" in cjsWatermarkMiss
|| "createdAtMs" in cjsWatermarkMiss
) {
throw new Error("The packed CommonJS tracked result decoder did not preserve the observed watermark miss");
throw new Error("The packed CommonJS tracked result decoder did not classify a watermark-fenced miss");
}
if (redisProtocol.decodeTrackedRedisFrame(redisProtocol.encodeRedisFrame("value", 1), null)?.payload !== "value") {
throw new Error("The packed CommonJS tracked decoder did not use zero for a missing watermark");
}
if (redisProtocol.decodeTrackedRedisReadResult(null, null) !== null) {
throw new Error("The packed CommonJS tracked result decoder did not preserve a generic miss without a watermark");
const cjsAbsentTrackedRead = redisProtocol.decodeTrackedRedisReadResult(null, null);
if (
cjsAbsentTrackedRead?.reason !== "value_absent"
|| "observedWatermarkMs" in cjsAbsentTrackedRead
|| "kind" in cjsAbsentTrackedRead
|| "payload" in cjsAbsentTrackedRead
) {
throw new Error("The packed CommonJS tracked result decoder did not classify an absent value");
}
const cjsAbsentTrackedReadWithWatermark = redisProtocol.decodeTrackedRedisReadResult(null, Buffer.from("7"));
if (
cjsAbsentTrackedReadWithWatermark?.kind !== "watermark_miss"
|| cjsAbsentTrackedReadWithWatermark.reason !== "value_absent"
|| cjsAbsentTrackedReadWithWatermark.observedWatermarkMs !== 7
|| "payload" in cjsAbsentTrackedReadWithWatermark
|| "createdAtMs" in cjsAbsentTrackedReadWithWatermark
) {
throw new Error("The packed CommonJS tracked result decoder did not preserve an absent-value refill fence");
}
if (
"REDIS_FRAME_VERSION" in redisProtocol
Expand Down Expand Up @@ -1759,6 +1865,7 @@ function typescriptConfig(include) {
moduleResolution: "Node16",
noEmit: true,
strict: true,
exactOptionalPropertyTypes: false,
},
include,
},
Expand Down
5 changes: 3 additions & 2 deletions src/datadog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
DisabledMetricLabels,
ErrorMetricLabels,
InvalidationMetricLabels,
MissMetricLabels,
SerializationMetricLabels,
ShadowValidationMetricLabels,
StaleRecoveryMetricLabels,
Expand Down Expand Up @@ -92,8 +93,8 @@ export class DatadogDialCacheMetrics implements DialCacheMetricsAdapter {
this.increment(this.metricNames.request, cacheTags(labels));
}

miss(labels: CacheMetricLabels): void {
this.increment(this.metricNames.miss, cacheTags(labels));
miss(labels: MissMetricLabels): void {
this.increment(this.metricNames.miss, { ...cacheTags(labels), reason: labels.reason });
}

disabled(labels: DisabledMetricLabels): void {
Expand Down
13 changes: 6 additions & 7 deletions src/dialcache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
type ShadowValidationOutcome,
} from "./metrics.js";
import {
isRedisReadMiss,
isRedisWatermarkMiss,
type DecodedRedisFrame,
type RedisCachePayload,
Expand Down Expand Up @@ -607,7 +608,7 @@ export class DialCache {
return result.value;
}

this.metrics?.miss(labelsFor(key, REQUEST_LOCAL_CACHE_LAYER));
this.metrics?.miss({ ...labelsFor(key, REQUEST_LOCAL_CACHE_LAYER), reason: "value_absent" });
const value = await this.getThroughSharedLayers(
key,
keyConfig,
Expand Down Expand Up @@ -1111,11 +1112,9 @@ export class DialCache {
if (abandonIfExpired()) {
return "timeout";
}
if (isRedisWatermarkMiss(readResult)) {
shadowFillConfig = start.remoteConfig;
shadowFillWatermarkMiss = readResult;
} else if (readResult === null) {
if (isRedisReadMiss(readResult) || readResult === null) {
shadowFillConfig = start.remoteConfig;
shadowFillWatermarkMiss = isRedisWatermarkMiss(readResult) ? readResult : undefined;
} else {
flight.cachedFrame = readResult;
}
Expand Down Expand Up @@ -1219,7 +1218,7 @@ export class DialCache {
if (originalFrame === null) {
return "timeout";
}
const confirmationFrame = isRedisWatermarkMiss(confirmationResult)
const confirmationFrame = isRedisReadMiss(confirmationResult)
? null
: confirmationResult;
if (confirmationFrame === null || !redisPayloadsEqual(originalFrame.payload, confirmationFrame.payload)) {
Expand Down Expand Up @@ -1316,7 +1315,7 @@ export class DialCache {
this.metrics?.request(labelsFor(key, CacheLayer.LOCAL));
this.metrics?.observeGet(labelsFor(key, CacheLayer.LOCAL), elapsedSeconds(start));
if (result.status === "miss") {
this.metrics?.miss(labelsFor(key, CacheLayer.LOCAL));
this.metrics?.miss({ ...labelsFor(key, CacheLayer.LOCAL), reason: "value_absent" });
}
return result;
} catch (error) {
Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type {
export { DialCacheContext } from "./context.js";
export type {
CacheMetricLabels,
CacheMissReason,
CoalescedMetricLabels,
CoalescingScope,
CompressionMetricLabels,
Expand All @@ -22,6 +23,7 @@ export type {
InvalidationMetricLabels,
MetricErrorKind,
MetricLayer,
MissMetricLabels,
SerializationMetricLabels,
ShadowValidationMetricLabels,
ShadowValidationOutcome,
Expand Down Expand Up @@ -61,6 +63,7 @@ export type {
RedisCachePayload,
RedisInvalidationRequest,
RedisReadContext,
RedisReadMiss,
RedisReadRequest,
RedisReadResult,
RedisWatermarkMiss,
Expand Down
Loading
Loading