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
10 changes: 6 additions & 4 deletions .github/workflows/formal-full.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,12 @@ jobs:
runs-on: ubuntu-latest
# Shared-library challenges now execute the importing profiles' partitions,
# including clean baselines and mutant checks cached by exact source closure.
# The expanded partition campaign was still running after an hour in
# run 35568655690. Leave room for the full campaign on slower runners
# without reducing models, histories or partitions.
timeout-minutes: 150
# Runs 36416779057 and 37307897418 on main needed 2 h 28 min, and run
# 36347923680 was cancelled at exactly 150 minutes with the same models that
# had passed in 2 h 08 min one commit earlier. Leave real headroom for the
# full campaign on slower runners without reducing models, histories or
# partitions.
timeout-minutes: 240
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
- uses: ./.github/actions/setup-validation
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ DialCache organizes caching into use cases, with runtime control and
observability for each one. Its TypeScript, Go, Rust and Python implementations share
behavioral contracts and documentation.

- **Off by default:** caching runs only inside an `enable()` scope.
- **Off by default:** readers cache only inside `enable()`. Maintenance calls (`invalidateRemote`, `delete`) always act.
- **Multi-layer:** request-local → process-local → Redis.
- **Runtime policies per use case:** layers, TTLs, and rollout ramps.
- **Targeted invalidation:** one call per entity for its tracked Redis results.
- **Targeted invalidation:** fence an entity's tracked Redis results, or delete one exact result from this request, this instance and Redis.
- **Coalescing:** same-key reads share work when a cache layer is active.
- **Fail-open:** cache failures fall back to the loader.
- **Stale-on-error (opt-in):** retained Redis values for selected source errors.
Expand Down
49 changes: 45 additions & 4 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,38 @@ Redis configuration and mutation failures reject; the method works outside an
enabled scope. Choose the buffer from the
[clock and in-flight-work contract](invalidation.md#choosing-futurebufferms).

## `delete`

`delete(options: CacheIdentityOptions): Promise<void>` removes one exact cached
result from Redis when configured, this instance's process-local LRU, and the
live outer request memo. It works outside `enable()` and inside nested
`disable()`, with no policy-provider lookup, TTL/ramp checks, or coalescing.
A missing entry succeeds, including on instances without Redis.

`CacheIdentityOptions` contains `keyType`, `useCase`, `key`, and optional
`trackForInvalidation` (default `false`). The instance supplies the namespace.
Every identity dimension must match the reader exactly, including arguments and
tracking mode. Using the wrong tracking mode targets a different key and can
therefore succeed without removing the intended entry. No value type,
serializer, or configuration is needed.

An existing `GetOrLoadOptions<T>` value is structurally compatible with
`CacheIdentityOptions`: pass it directly. A registered reader's `CachedOptions`
is not an identity because it contains a `cacheKey` selector; invoke that
selector with the reader's arguments and supply the resulting `key` instead.
The shared `CacheUseCaseOptions` base names the use case and tracking mode.

Validation and adapter-capability checks happen before mutation or the deletion
metric. Redis deletion completes first; a remote failure leaves both memory
stores intact. Then local and live memo removal run synchronously. A configured
adapter without optional `delete` rejects with `RemoteDeleteUnsupportedError`;
other mutation failures also reject and never report partial removal as success.

Deletion changes no watermarks, sibling identities, other instances or requests,
flights, acquired snapshots, or shadow jobs. A previously admitted load can
publish after deletion. See [exact-key deletion versus entity invalidation](invalidation.md#exact-key-deletion-versus-entity-invalidation)
for examples and concurrency boundaries.

## `getCoalescingState`

`getCoalescingState(): CoalescingState` returns a point-in-time process-flight
Expand All @@ -282,7 +314,8 @@ process.oldestLeaderAgeMs; // Monotonic age, or null when idle.

The nested shape is `ProcessCoalescingState`. Request-local flights are excluded.
There is no method to clear a cache, cancel in-flight loads, cap coalesced
flights, or shut an instance down.
flights, or shut an instance down. [`delete`](#delete) removes one exact result,
not an entire cache.
See [Coalescing state](coalescing.md#inspecting-process-scoped-flights).

## Keys and serializers
Expand Down Expand Up @@ -350,9 +383,10 @@ braces; it does not encode the value. Neither helper adds arguments or a use cas

| Root export | When it matters |
| --- | --- |
| `DialCacheError` | Base class of the four core errors below |
| `DialCacheError` | Base class of the core errors below |
| `UseCaseIsAlreadyRegisteredError` | Duplicate `cached()` registration on an instance |
| `UseCaseNameIsReservedError` | Either operation API uses `"watermark"` |
| `UseCaseNameIsReservedError` | A reader or exact deletion uses `"watermark"` |
| `RemoteDeleteUnsupportedError` | Exact deletion has a configured Redis adapter without the optional `delete` capability |
| `FallbackTimeoutError` | Enabled source deadline; exposes `useCase` and `timeoutMs` |
| `RedisReadTimeoutError` | Remote wait deadline; exposes `useCase` and `timeoutMs`; serving reads log/count it before fallback, while shadow reads report a job outcome |
| `DialCacheRedisPayloadError` | Invalid raw Redis reply shape |
Expand All @@ -368,11 +402,18 @@ Source errors retain their original rejection value if recovery does not serve.

`RedisConfig`, `CompressionConfig`, `DialCacheRedisClient`, `RedisReadRequest`,
`RedisReadContext`, `RedisReadResult`, `RedisReadMiss`, `DecodedRedisFrame`,
`RedisWriteRequest`, `RedisInvalidationRequest`, and `RedisCachePayload` are root
`RedisWriteRequest`, `RedisInvalidationRequest`, `RedisDeleteRequest`, and `RedisCachePayload` are root
types. Use `isRedisReadMiss` to discriminate reads. The complete semantic and
binary contracts are in [Redis and Valkey](redis.md#custom-client-contract).

`DialCacheRedisClient.delete?(request: RedisDeleteRequest)` is optional and
returns `void` or a promise. Its request contains the exact `valueKey`; remove
that key only, never an entity watermark. Existing adapters retain read/write
compatibility and report unsupported if deletion is requested.

`DialCacheMetricsAdapter` and its label/outcome types are root exports.
The optional `deletion(labels)` hook counts attempts after validation, with
namespace, use case, key type and deepest configured layer (`remote` or `local`).
[Observability](observability.md#custom-adapters) lists required and optional
hooks, bounded labels, and the effects of omitting optional hooks.

Expand Down
13 changes: 12 additions & 1 deletion docs/concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,10 @@ its loader with its enabled fallback deadline.

Keep mutation work outside the enabled boundary or inside a disabled region.
Disabling does not invalidate anything; mutable data still needs appropriate TTLs
or [targeted invalidation](invalidation.md).
or [targeted invalidation](invalidation.md). Explicit maintenance operations
ignore enabled state: entity invalidation always attempts its remote mutation,
and exact deletion removes the targeted local/memo entries and configured remote
value, even inside a disabled region. Deletion never creates a request memo.

## Three lifetimes

Expand Down Expand Up @@ -158,6 +161,14 @@ evict values already in request-local or process-local memory. A Redis hit can
also warm the local layer with a full local TTL; the remote TTL is not an
end-to-end maximum age across the chain.

| Maintenance | Memory stores | Redis | Concurrent work |
| --- | --- | --- | --- |
| Entity invalidation | Existing entries retain their lifetimes | Advances one entity's tracked watermark | Existing flights and acquired snapshots remain usable |
| Exact-key deletion | Removes one key in this instance and live request | Removes one value key; preserves watermarks | Existing flights and snapshots remain; a late load can repopulate |

See [exact-key deletion](invalidation.md#exact-key-deletion-versus-entity-invalidation)
for identity matching and failure ordering.

Changing policy does not evict old entries. In particular, a shorter local TTL
applies to new writes; existing local values retain their insertion TTL. Redis
reads classify frame age using the current remote policy. See
Expand Down
117 changes: 117 additions & 0 deletions docs/invalidation.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,123 @@ Invalidation works outside an enabled scope. Call it **after the source mutation
commits** and surface failures to the application's maintenance path.
[Redis setup](redis.md) covers client connections and ownership.

## Exact-key deletion versus entity invalidation

Use exact deletion when one known result must be removed, including untracked
Redis entries or local-only caching. Use entity invalidation when every tracked
argument and use-case variant of an entity must observe a timestamp fence.
They can be used together after the source mutation commits.

| Property | Exact-key deletion | Entity invalidation |
| --- | --- | --- |
| Scope | One namespace, key type, ID, use case, argument set and tracking mode | All tracked use cases and arguments of one namespace, key type and ID |
| Stores | Configured Redis value, this instance's local entry, this live request's memo | Redis entity watermark |
| Untracked results | Supported | Unchanged |
| Watermarks | Never read, changed or removed | Monotonically advanced |
| Concurrent load guarantee | Removes existing entries; an earlier load may publish again | Future tracked reads enforce the cutoff, subject to clock and in-flight-work bounds |
| Failure behavior | Unsupported adapter fails before any store changes; remote failure preserves local/memo state | Missing remote or mutation failure surfaces; existing memory entries remain |

Both maintenance operations ignore enablement. Deletion works outside a request
and inside nested disabled scopes. It never resolves runtime policy, checks TTL
or ramps, coalesces work, or creates a request memo. A missing key succeeds;
without a remote adapter, local and live request removal still run.

The delete identity must match the reader exactly, including arguments and
tracking mode. Tracking changes the key prefix. Deleting with the wrong mode
can successfully remove nothing because a missing key is not an error.

After identity validation, deletion verifies the configured adapter's capability
and counts the attempt. It awaits remote deletion first, then removes the local
entry and live request memo synchronously. Remote-first ordering prevents a
local miss from refilling from an old remote frame between those steps. A remote
error leaves memory entries intact; failures surface to the maintenance caller.
A failed dispatched command may already have executed, so retry explicitly when
appropriate. Rust custom local-store failures can surface after remote removal;
partial removal is never success.

Deletion leaves siblings, other namespaces and instances, other requests'
memos, watermarks, flights, and shadow jobs alone. Two races remain intentional:

- **Late publication:** a load admitted before deletion can finish afterward and
repopulate the deleted key. Deletion does not cancel it or detach its flight.
- **Acquired snapshot:** a caller that already obtained a value, including bytes
waiting for decoding or stale recovery, can return that value after deletion.
It also publishes that value into the request memo and local store, where it
then lives for a full local TTL.

The following executable example warms an untracked local result, deletes it
within a live request, and verifies that the next read loads the changed source.
With Redis configured, the same API also removes the remote value first.

<LanguageContent language="typescript">

<<< @/../typescript/examples/docs.mts#exact-delete{typescript}

[Complete executable example](https://github.com/lan17/DialCache/blob/main/typescript/examples/docs.mts)

</LanguageContent>

<LanguageContent language="go">

<<< @/../go/docs_examples_test.go#exact-delete{go}

[Complete executable example](https://github.com/lan17/DialCache/blob/main/go/docs_examples_test.go)

</LanguageContent>

<LanguageContent language="rust">

<<< @/../rust/tests/docs_examples.rs#exact-delete{rust}

[Complete executable example](https://github.com/lan17/DialCache/blob/main/rust/tests/docs_examples.rs)

</LanguageContent>

<LanguageContent language="python">

<<< @/../python/tests/test_docs_examples.py#exact-delete{python}

[Complete executable example](https://github.com/lan17/DialCache/blob/main/python/tests/test_docs_examples.py)

</LanguageContent>

<LanguageContent language="typescript">

Call `await dialcache.delete({ keyType, useCase, key, trackForInvalidation })`.
A `GetOrLoadOptions<T>` value can be reused directly. For a registered reader,
recompute its `cacheKey` selector for the arguments; the selector itself is not
an identity. Unsupported adapters reject with `RemoteDeleteUnsupportedError`.

</LanguageContent>

<LanguageContent language="go">

Call `cache.Delete(ctx, identity)`. An empty identity namespace inherits the
instance's. The live outer request comes from `ctx` even inside `Disable`;
pass a context without that request to remove only shared stores. A configured
remote must implement the optional `RemoteDeleter` interface or the call returns
`ErrDeleteUnsupported`.

</LanguageContent>

<LanguageContent language="rust">

Call `cache.delete(&scope, identity).await`; use `Scope::outside()` without a
request. An empty identity namespace inherits the instance's. Custom remotes
opt in with `supports_delete` and `delete`, otherwise the operation returns
`Error::RemoteDeleteUnsupported`.

</LanguageContent>

<LanguageContent language="python">

Call `await cache.delete(key=..., key_type=..., use_case=...,
track_for_invalidation=False)`. Scope comes from the live context, including
inside `disable()`. Custom clients can implement `RedisDeleteClient`; a missing
`delete` method raises `RemoteDeleteUnsupportedError`.

</LanguageContent>

## Choosing `futureBufferMs`

The buffer covers stale work that can still become visible after invalidation.
Expand Down
5 changes: 5 additions & 0 deletions docs/languages/go.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ and `errors.As` for native error handling. `FallbackTimeoutError` reports a
source deadline; callback panics become `CallbackPanicError`. `Invalidate`
returns maintenance errors, including `ErrNoRemote` without an adapter.

`cache.Delete(ctx, identity)` removes one exact result; an empty identity
namespace inherits the cache's. The live request memo follows `ctx`, including
inside `Disable`. Custom remotes opt in through `RemoteDeleter`; otherwise the
call returns `ErrDeleteUnsupported`. See [exact deletion](../invalidation.md#exact-key-deletion-versus-entity-invalidation).

## Identity and values

`Identity` holds normalized strings and ordered argument pairs. Use
Expand Down
8 changes: 8 additions & 0 deletions docs/languages/python.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ exercise both forms of identity, ordinary and canceled concurrent callers,
deadline boundaries, sparse runtime policies, and argument adaptation through
the public API.

`await cache.delete(key=..., key_type=..., use_case=...,
track_for_invalidation=False)` removes one exact result. It uses the existing
live context even inside `disable()` and is independent of runtime policy.
For deletion, `key` must be a scalar ID or `{"id": ..., "args": {...}}`; a
prebuilt `Key` raises `TypeError` before any cache state changes.
Custom clients may implement `RedisDeleteClient`; missing support raises
`RemoteDeleteUnsupportedError`. See [exact deletion](../invalidation.md#exact-key-deletion-versus-entity-invalidation).

## Identity and policy

Prefer `cache_key=` to select identity explicitly, as shown in the shared
Expand Down
8 changes: 7 additions & 1 deletion docs/languages/rust.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ dropping the guard closes it. `enable` offers a callback form. `enable_in` and
`disable_in` derive nested scopes; `Scope::outside()` runs without caching.
Retained scope clones no longer enable caching after the outer scope closes.

`cache.delete(&scope, identity).await` removes one exact result. Pass
`Scope::outside()` without a request; an empty identity namespace inherits the
cache's. Custom remotes override `supports_delete` and `delete`, or the call
returns `Error::RemoteDeleteUnsupported` before mutation. Local-store removal
failures surface as `Error::Local`. See [exact deletion](../invalidation.md#exact-key-deletion-versus-entity-invalidation).

## Policy and errors

Static `Policy` builders use whole seconds for TTL/recovery ages and milliseconds
Expand All @@ -64,7 +70,7 @@ registration return native errors before work begins.
Source errors appear as `Error::Source(Arc<dyn Error>)`; coalesced callers share
that error instance. A source deadline produces `Error::FallbackTimeout` and
does not cancel raw source work. Dropping a caller's returned future also does
not cancel the execution or other followers. Explicit `invalidate` errors are
not cancel the execution or other followers. Explicit `invalidate` and `delete` errors are
returned to the maintenance caller. Cache plumbing fails open.

## Identity and values
Expand Down
8 changes: 7 additions & 1 deletion docs/languages/typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ stops enabling new work and can no longer accept request-local publication.
always return promises. Keep the source arguments and captured state immutable
when detached shadow work can use them later.

`await dialcache.delete(identity)` accepts `CacheIdentityOptions` or an existing
`GetOrLoadOptions<T>` value. For registered readers, compute `key` with the
reader's selector and copy its use case, key type, and tracking mode. Explicit
maintenance runs outside enablement and failures reject. See
[exact deletion](../invalidation.md#exact-key-deletion-versus-entity-invalidation).

## Policy and errors

`DialCacheKeyConfig` supplies static defaults and sparse runtime overlays from
Expand All @@ -42,7 +48,7 @@ without canceling work or evicting data.

Static option validation throws. Cache plumbing errors fail open to the source;
source throws and rejections retain their identity. `FallbackTimeoutError`
identifies the library's source deadline. Explicit `invalidateRemote` returns a
identifies the library's source deadline. Explicit `invalidateRemote` or `delete` returns a
promise whose failure must be handled by maintenance code.

A deadline cannot preempt synchronous JavaScript. Bound dependency work and
Expand Down
Loading
Loading