Skip to content

Provide atomic multi-key operations across KV adapters #1361

Open
0 of 7 issues completed
Open

Description

@dahlia

Motivation

A worker may need to record a notice and update its admission budget together, or insert fanout children while advancing their parent's cursor. KvStore currently has optional single-key cas(), but separate reads and writes cannot express either transition. Applications otherwise need backend-specific stores or a large single compare-and-swap (CAS) record that serializes unrelated work.

Add a reusable atomic KV contract and implement it in the existing adapters. Delivery suspension is one consumer, but the contract should also serve queues and application state without requiring FEP-f799. This umbrella tracks that storage work separately from the delivery-suspension umbrella.

Proposed surface

KvStore gains optional getMany(), setMany(), deleteMany(), and casMany(). AtomicKvStore requires those operations and the existing cas(). Reads have a consistent snapshot; a conditional batch commits every change only if every expected value matches. The child contract issue defines equality, time to live (TTL), limits, deletion and error semantics.

A proposed use is reserving a record and its budget in one commit:

import type { AtomicKvStore } from "@fedify/fedify";

declare const store: AtomicKvStore;
const recordKey = ["example", "reservation"] as const;
const budgetKey = ["example", "budget"] as const;
const [record, budget] = await store.getMany([recordKey, budgetKey]);

const committed = await store.casMany([
  { key: recordKey, expectedValue: record, newValue: nextRecord },
  { key: budgetKey, expectedValue: budget, newValue: nextBudget },
]);
// On false, re-read and re-evaluate the application rule before retrying.

nextRecord and nextBudget are application-computed values. This is a proposed API, not an example supported by today's package.

Children and release boundaries

Each child is independently releasable. The contract child includes a working Memory implementation rather than type-only scaffolding. Production consumers can ship once an appropriate persistent adapter is ready; they do not wait for every adapter or this umbrella to close. Other existing stores remain valid KvStore implementations, with their atomic support recorded in the support matrix and further implementations tracked separately.

Design choices

Reuse existing adapter instances instead of introducing a separate suspension, publisher and backlog store for each backend. Keep the new methods optional on KvStore so unrelated cache users and third-party adapters retain their existing contract. Use a required subinterface when a consumer needs stronger operations.

Single-key CAS is still useful, but a single global state document would increase contention and object size. A distributed lock assembled from get() and set() does not provide the missing atomic contract. A general asynchronous transaction callback would also expose SQL-shaped behavior that other backends cannot necessarily implement. A declarative conditional batch gives callers the transition they need without that callback API.

AtomicKvStore describes per-operation atomicity. It does not imply durable storage, a transaction spanning successive calls, or exactly-once external effects. Redis Cluster key scope, adapter codecs and batch limits must be tested rather than inferred from method presence.

Completion and documentation

Complete each child's conformance tests, public JSDoc, adapter examples, package changelog and relevant skills in that child's implementation. Expand docs/manual/kv.md with the contract, adapter support matrix, custom implementation guidance and the distinction between cache cleanup and authoritative state. Document the shared test helper in packages/testing/README.md. Keep adapter-specific restrictions in the matching package README and KV section.

Target this umbrella and its seven children at Fedify 2.5. Delivery suspension remains a separate release decision.

Related work: #1362.

Metadata

Metadata

Assignees

No one assigned

    Labels

    component/kvKey–value store relatedcomponent/testingTesting utilities (@fedify/testing)

    Type

    Fields

    Priority

    None yet

    Effort

    None yet

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions