Skip to content

Add Nebulex.Adapter.CompositeKV behaviour for composite KV operations - #255

Merged
cabol merged 3 commits into
mainfrom
feat/composite-kv-behaviour
Sep 27, 2026
Merged

cabol merged 3 commits into
mainfrom
feat/composite-kv-behaviour

Conversation

@cabol

@cabol cabol commented Sep 6, 2026 •

Copy link
Copy Markdown
Member

Problem

The composite KV operations get_and_update/3, update/4, fetch_or_store/3, and get_or_store/3 were not adapter callbacks. They lived only in Nebulex.Cache.KV, composed on top of the primitive fetch, put, and delete commands, with the given function always evaluated in the calling process.

Adapters had no way to change that. The Nebulex.Adapters.Partitioned docs claimed the supplied function ran on the node owning the key, but only the primitive commands were routed there; the function itself always ran locally, as elixir-nebulex/nebulex_distributed#19 reports. For the workload in that issue, where computing each value requires a large amount of memory in a lower-level cache, every node must hold enough memory for the whole key space instead of its own partition, so the cache cannot scale that workload by adding nodes.

Change

Add Nebulex.Adapter.CompositeKV, which specifies the four operations as adapter callbacks. use Nebulex.Adapter.CompositeKV provides the default implementation, moved out of Nebulex.Cache.KV, with every callback overridable.

The behaviour is required. use Nebulex.Cache validates at compile time that the adapter implements Nebulex.Adapter, Nebulex.Adapter.KV, and Nebulex.Adapter.CompositeKV, naming the missing one otherwise, and Nebulex.Cache.CompositeKV dispatches each command to the adapter callback like every other cache command. Adapters implement the callbacks to change where the function runs or to provide different atomicity guarantees; use Nebulex.Adapter.CompositeKV gives them the default.

Nebulex.Cache gains the public types get_and_update_fun/0 and update_fun/0 for the callback signatures, and ttl/0 and keep_ttl/0, which Nebulex.Adapter.KV and the expire callbacks now reuse instead of defining their own.

Three shared suites cover the operations. Nebulex.Cache.CompositeKVTest and Nebulex.Cache.CompositeKVTelemetryTest are registered in Nebulex.CacheTestCase; Nebulex.Cache.CompositeKVErrorTest is used by Nebulex.CacheErrorTest alongside the other error suites.

The adapter-authoring guide, the getting-started guide, and usage-rules/nebulex.md are updated for the behaviour.

Closes #254.

Release notes

New

  • Nebulex.Adapter.CompositeKV, an adapter behaviour for the composite KV operations get_and_update, update, fetch_or_store, and get_or_store. Adapters can implement the callbacks to change where the given function runs or to provide different atomicity guarantees; use Nebulex.Adapter.CompositeKV provides the default, non-atomic implementation with all callbacks overridable.
  • Composite operations are now adapter commands and emit their own Telemetry command span in addition to the primitive spans. Handlers see four new :command values: :get_and_update, :update, :fetch_or_store, and :get_or_store. A handler that matches command exhaustively without a fallback clause will raise and be detached on the first composite call.
  • New public types Nebulex.Cache.get_and_update_fun/0, Nebulex.Cache.update_fun/0, Nebulex.Cache.ttl/0, and Nebulex.Cache.keep_ttl/0.

Fixed

  • Nebulex.Adapter.run_command/4 no longer strips the shared :telemetry, :telemetry_event, and :telemetry_metadata options before invoking the adapter, reversing the v3.0.0 behavior of extracting them to keep adapter arguments clean. Nested commands, such as the primitive commands run by a composite operation, now receive them: telemetry: false disables the nested spans and the cache entry events and stats derived from them, :telemetry_metadata reaches the nested spans, and a custom :telemetry_event applies to them. Adapters that forward :telemetry_metadata to their backend, such as the Redis adapter, now receive it. The options appear in the :args Telemetry metadata as the adapter receives them.

Behavior changes

  • get_and_update/3 no longer treats a nil new value as "do not write". A function returning {current, nil} stores nil under the key and returns {current, nil}, instead of skipping the write and returning {current, current}. That short-circuit came from v2, where nil was the cache-miss sentinel; v3 treats nil as a valid cache value, and update/4 already stored it.
  • get_and_update/3 with :pop now removes an entry holding nil. The default implementation mapped a missing key to nil before handing it to the function, which also made a cached nil look like a miss, so :pop skipped the delete. A missing key still issues no delete command.

For adapter developers

Adapters must implement Nebulex.Adapter.CompositeKV; a cache using an adapter that does not fails to compile with a message naming the behaviour. Add use Nebulex.Adapter.CompositeKV for the default implementation, or implement the callbacks. Two more things may require changes. Adapter callbacks now receive the shared :telemetry, :telemetry_event, and :telemetry_metadata options in opts, so a callback that validates its options strictly must drop them first with Keyword.drop(opts, Nebulex.Cache.Options.__runtime_shared_opts__()). Adapter repositories running the shared test suite need the composite helpers in their test cache module: fetch_or_store_fun/0, fetch_or_store_error_fun/0, and get_or_store_fun/0.

The built-in cache entry and stats handlers are driven by the primitive command events. If an override bypasses those commands, or runs them under another cache's metadata, those handlers will not account for the operation on the original cache. Core cannot derive the events from the composite command: adding handler clauses for it would double-count on the default path, which already emits the primitive events, and fetch_or_store/get_or_store return {:ok, value} on both a hit and a miss, so the composite event does not say whether anything was written. Overrides should preserve equivalent events for the affected cache.

Validation

mix precommit passes: 435 tests, 100% coverage, Dialyzer 0 errors, Credo strict, format, Sobelow, deps.audit, doctor, and mix docs with no warnings.

Next

Nebulex.Adapters.Partitioned in nebulex_distributed can implement the callbacks to run the given function on the node owning the key, resolving nebulex_distributed#19. The other adapters in that repository need only use Nebulex.Adapter.CompositeKV.

@codecov

codecov Bot commented Sep 6, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Flag Coverage Δ
unittests-elixir-1.20.x-otp-29.x 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
lib/nebulex/adapter.ex 100.00% <100.00%> (ø)
lib/nebulex/adapter/composite_kv.ex 100.00% <100.00%> (ø)
lib/nebulex/adapters/nil.ex 100.00% <ø> (ø)
lib/nebulex/cache.ex 100.00% <100.00%> (ø)
lib/nebulex/cache/composite_kv.ex 100.00% <100.00%> (ø)
lib/nebulex/cache/kv.ex 100.00% <ø> (ø)
lib/nebulex/cache/options.ex 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@cabol
cabol force-pushed the feat/composite-kv-behaviour branch 2 times, most recently from cb079fe to 3808f9c Compare September 27, 2026 14:33
`Nebulex.Adapter.run_command/4` popped `:telemetry`, `:telemetry_event`,
and `:telemetry_metadata` from the options before invoking the adapter, a
choice made in v3.0.0 to keep adapter arguments clean. As a result, nested
commands did not receive the shared telemetry options. With
`telemetry: false`, the outer span was disabled while nested spans still
emitted events and updated statistics. `:telemetry_metadata` never reached
the nested spans, and adapters that forward it to their backend, such as
the Redis adapter, never received it. A custom `:telemetry_event` applied
only to the outer span.

Read the options with `Keyword.get/3` instead, leaving them in place, so
nested commands receive the forwarded options. They appear in the `:args`
Telemetry metadata as the adapter receives them. Adapters that validate
their options strictly must drop the shared runtime options first, as the
test adapter's `transaction/3` now does.

Refs #254
@cabol
cabol force-pushed the feat/composite-kv-behaviour branch 4 times, most recently from effdd4c to 41f66f4 Compare September 27, 2026 15:14
@cabol
cabol requested a lite review from Copilot September 27, 2026 15:14
@cabol
cabol force-pushed the feat/composite-kv-behaviour branch from 41f66f4 to 8d667ea Compare September 27, 2026 15:18

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

A moderate compatibility issue remains in fetch_or_store/6 and get_or_store/6 when adapters return {:ok, false} from put.

Review effort: Lite
Findings: 2 Low severity

Open (2)
What changed in this PR

Adds adapter-level CompositeKV callbacks with default implementations, telemetry support, updated public types, tests, and documentation.

Changes:

  • Introduces Nebulex.Adapter.CompositeKV and cache-layer dispatch.
  • Updates telemetry forwarding and composite operation semantics.
  • Adds shared coverage, guides, architecture rules, and release notes.
File Description
usage-rules/​nebulex.md Documents CompositeKV behavior.
usage-rules/​architecture.md Updates architecture guidance.
test/​support/​test_cache.exs Adds composite test helpers.
test/​support/​test_adapter.exs Handles shared runtime options.
test/​support/​fake_adapter.exs Uses default CompositeKV behavior.
test/​shared/​cache/​kv_test.exs Updates KV test coverage.
test/​shared/​cache/​composite_kv_test.exs Tests composite operations.
test/​shared/​cache/​composite_kv_telemetry_test.exs Tests composite telemetry.
test/​shared/​cache/​composite_kv_error_test.exs Tests composite errors.
test/​shared/​cache_test_case.exs Registers shared suites.
test/​nebulex/​telemetry_test.exs Tests telemetry option forwarding.
test/​nebulex/​cache_test.exs Updates cache behavior tests.
test/​nebulex/​cache_error_test.exs Registers composite error tests.
mix.exs Updates documentation configuration.
lib/​nebulex/​cache/​kv.ex Removes relocated implementations.
lib/​nebulex/​cache/​composite_kv.ex Dispatches composite operations.
lib/​nebulex/​cache.ex Adds APIs, types, and documentation.
lib/​nebulex/​adapter/​kv.ex Reuses shared cache types.
lib/​nebulex/​adapter/​composite_kv.ex Defines callbacks and defaults.
lib/​nebulex/​adapter.ex Adds telemetry-aware command execution.
guides/​learning/​creating-new-adapter.md Documents adapter behavior.
guides/​introduction/​getting-started.md Updates usage guidance.
CHANGELOG.md Records release changes.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread lib/nebulex/cache/composite_kv.ex Outdated
Comment thread usage-rules/architecture.md Outdated
@cabol
cabol force-pushed the feat/composite-kv-behaviour branch 3 times, most recently from e96a268 to 0cb78e7 Compare September 27, 2026 15:33
The composite KV operations `get_and_update/3`, `update/4`,
`fetch_or_store/3`, and `get_or_store/3` were not adapter callbacks. They
lived only in `Nebulex.Cache.KV`, composed on top of the primitive `fetch`,
`put`, and `delete` commands, with the given function always evaluated in
the calling process. Adapters had no way to change that, so a distributed
adapter could not run the function where the key lives.

Add `Nebulex.Adapter.CompositeKV`, which specifies the four operations as
adapter callbacks. `use Nebulex.Adapter.CompositeKV` provides the default
implementation, moved out of `Nebulex.Cache.KV`, with every callback
overridable. Every cache keeps exposing the four functions:
`Nebulex.Cache.CompositeKV` dispatches each command to the adapter when it
implements the callback and to the default implementation otherwise, both
under the same Telemetry command span through the new
`Nebulex.Adapter.run_command/5`. Adapters need no changes to retain the
composite functions; they implement the callbacks to change where the
function runs or to provide different atomicity guarantees.

Since the operations are now adapter commands, each emits its own
Telemetry command span in addition to the primitive spans it triggers.
The write options `:ttl` and `:keep_ttl` are validated once by the cache
layer and passed to the callbacks as arguments, and `ttl/0` and
`keep_ttl/0` move to `Nebulex.Cache` alongside the other shared types.

Two `get_and_update/3` bugs inherited from v2 are fixed. A function
returning `{current, nil}` skipped the write and returned
`{current, current}`, a leftover from when `nil` was the cache-miss
sentinel; it now stores `nil` like any other value. And `:pop` skipped the
delete for an entry holding `nil`, because the read collapsed a cached
`nil` and a missing key into the same value. The implementation
distinguishes `{:hit, value}` from `:miss`.

Tests move to shared suites, so adapters running them are checked against
the same contract: `Nebulex.Cache.CompositeKVTest`,
`Nebulex.Cache.CompositeKVErrorTest`, and
`Nebulex.Cache.CompositeKVTelemetryTest`, the last of which checks the
default implementation. The guides and usage rules are updated for the new
behaviour.

The CHANGELOG gains an Unreleased section covering these changes and the
others since v3.0.4.

Closes #254
`Nebulex.Adapter` listed `__before_compile__/1` as optional, but
`Nebulex.Cache` registers `@before_compile adapter` unconditionally, so an
adapter without it fails to compile any cache that uses it. Drop the
optional marker so the behaviour states the actual requirement and the
compiler reports a missing implementation on the adapter itself.
@cabol
cabol force-pushed the feat/composite-kv-behaviour branch from 0cb78e7 to 93222e7 Compare September 27, 2026 15:47
@cabol
cabol merged commit fcc0b82 into main Sep 27, 2026
6 checks passed
@cabol
cabol deleted the feat/composite-kv-behaviour branch September 27, 2026 15:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Nebulex.Adapter.CompositeKV behaviour for composite KV operations

2 participants