Repository navigation
Add Nebulex.Adapter.CompositeKV behaviour for composite KV operations - #255
Merged
Merged
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests.
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
cabol
force-pushed
the
feat/composite-kv-behaviour
branch
2 times, most recently
from
September 27, 2026 14:33
cb079fe to
3808f9c
Compare
`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
force-pushed
the
feat/composite-kv-behaviour
branch
4 times, most recently
from
September 27, 2026 15:14
effdd4c to
41f66f4
Compare
cabol
force-pushed
the
feat/composite-kv-behaviour
branch
from
September 27, 2026 15:18
41f66f4 to
8d667ea
Compare
There was a problem hiding this comment.
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
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.CompositeKVand 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.
cabol
force-pushed
the
feat/composite-kv-behaviour
branch
3 times, most recently
from
September 27, 2026 15:33
e96a268 to
0cb78e7
Compare
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
force-pushed
the
feat/composite-kv-behaviour
branch
from
September 27, 2026 15:47
0cb78e7 to
93222e7
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Problem
The composite KV operations
get_and_update/3,update/4,fetch_or_store/3, andget_or_store/3were not adapter callbacks. They lived only inNebulex.Cache.KV, composed on top of the primitivefetch,put, anddeletecommands, with the given function always evaluated in the calling process.Adapters had no way to change that. The
Nebulex.Adapters.Partitioneddocs 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.CompositeKVprovides the default implementation, moved out ofNebulex.Cache.KV, with every callback overridable.The behaviour is required.
use Nebulex.Cachevalidates at compile time that the adapter implementsNebulex.Adapter,Nebulex.Adapter.KV, andNebulex.Adapter.CompositeKV, naming the missing one otherwise, andNebulex.Cache.CompositeKVdispatches 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.CompositeKVgives them the default.Nebulex.Cachegains the public typesget_and_update_fun/0andupdate_fun/0for the callback signatures, andttl/0andkeep_ttl/0, whichNebulex.Adapter.KVand theexpirecallbacks now reuse instead of defining their own.Three shared suites cover the operations.
Nebulex.Cache.CompositeKVTestandNebulex.Cache.CompositeKVTelemetryTestare registered inNebulex.CacheTestCase;Nebulex.Cache.CompositeKVErrorTestis used byNebulex.CacheErrorTestalongside the other error suites.The adapter-authoring guide, the getting-started guide, and
usage-rules/nebulex.mdare updated for the behaviour.Closes #254.
Release notes
New
Nebulex.Adapter.CompositeKV, an adapter behaviour for the composite KV operationsget_and_update,update,fetch_or_store, andget_or_store. Adapters can implement the callbacks to change where the given function runs or to provide different atomicity guarantees;use Nebulex.Adapter.CompositeKVprovides the default, non-atomic implementation with all callbacks overridable.:commandvalues::get_and_update,:update,:fetch_or_store, and:get_or_store. A handler that matchescommandexhaustively without a fallback clause will raise and be detached on the first composite call.Nebulex.Cache.get_and_update_fun/0,Nebulex.Cache.update_fun/0,Nebulex.Cache.ttl/0, andNebulex.Cache.keep_ttl/0.Fixed
Nebulex.Adapter.run_command/4no longer strips the shared:telemetry,:telemetry_event, and:telemetry_metadataoptions 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: falsedisables the nested spans and the cache entry events and stats derived from them,:telemetry_metadatareaches the nested spans, and a custom:telemetry_eventapplies to them. Adapters that forward:telemetry_metadatato their backend, such as the Redis adapter, now receive it. The options appear in the:argsTelemetry metadata as the adapter receives them.Behavior changes
get_and_update/3no longer treats anilnew value as "do not write". A function returning{current, nil}storesnilunder the key and returns{current, nil}, instead of skipping the write and returning{current, current}. That short-circuit came from v2, wherenilwas the cache-miss sentinel; v3 treatsnilas a valid cache value, andupdate/4already stored it.get_and_update/3with:popnow removes an entry holdingnil. The default implementation mapped a missing key tonilbefore handing it to the function, which also made a cachednillook like a miss, so:popskipped 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. Adduse Nebulex.Adapter.CompositeKVfor the default implementation, or implement the callbacks. Two more things may require changes. Adapter callbacks now receive the shared:telemetry,:telemetry_event, and:telemetry_metadataoptions inopts, so a callback that validates its options strictly must drop them first withKeyword.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, andget_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_storereturn{: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 precommitpasses: 435 tests, 100% coverage, Dialyzer 0 errors, Credo strict, format, Sobelow,deps.audit,doctor, andmix docswith no warnings.Next
Nebulex.Adapters.Partitionedin 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 onlyuse Nebulex.Adapter.CompositeKV.