Skip to content

Commit 396d397

Browse files
committed
[#6] Add helper for managing cache reference entries
1 parent fc4aa03 commit 396d397

File tree

5 files changed

+262
-0
lines changed

5 files changed

+262
-0
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,17 @@
3939
the developer experience when working with the queryable API, especially for
4040
tag-based queries. Example: `match_spec value: v, tag: t, where: t == :group_a, select: v`.
4141
[#5](https://github.com/elixir-nebulex/nebulex_local/issues/5).
42+
- [Nebulex.Adapters.Local] Added `keyref_match_spec/2` helper function to
43+
`Nebulex.Adapters.Local.QueryHelper` for managing cache reference entries
44+
(keyrefs). This helper simplifies finding and cleaning up reference entries
45+
created when using the `:references` option with `Nebulex.Caching` decorators.
46+
Users can now easily invalidate all cached representations of an entity with a
47+
simple function call, without needing to know the internal keyref structure
48+
`{:"$nbx_keyref_spec", cache, key, ttl}`. Example:
49+
`keyref_match_spec(:user_123) |> MyCache.delete_all!(query: ...)`. This works
50+
seamlessly with `get_all/1`, `count_all/1`, and `delete_all/1` operations, and
51+
supports optional cache filtering.
52+
[#6](https://github.com/elixir-nebulex/nebulex_local/issues/6).
4253

4354
### Backwards incompatible changes
4455

lib/nebulex/adapters/local.ex

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,79 @@ defmodule Nebulex.Adapters.Local do
413413
MyApp.Cache.get_all!(query: MyApp.CacheQueries.user_entries(123))
414414
MyApp.Cache.delete_all!(query: MyApp.CacheQueries.expiring_soon())
415415
416+
### Working with cache references
417+
418+
When using the `:references` option with `Nebulex.Caching` decorators (like
419+
`@decorate cacheable/3`), Nebulex creates reference entries to track
420+
dependencies between cached values. The `keyref_match_spec/2` helper makes it
421+
easy to find and clean up these reference entries.
422+
423+
The function builds a match spec that finds all cache keys (reference keys)
424+
that point to a specific referenced key. This is useful for:
425+
426+
* Invalidating all entries that depend on a specific key
427+
* Counting how many references point to a key
428+
* Getting a list of dependent cache keys
429+
430+
#### Examples
431+
432+
import Nebulex.Adapters.Local.QueryHelper
433+
434+
# Delete all references to a specific key (any cache)
435+
ms = keyref_match_spec(:user_123)
436+
MyCache.delete_all!(query: ms)
437+
438+
# Delete references in a specific cache only
439+
ms = keyref_match_spec(:user_123, cache: MyApp.UserCache)
440+
MyCache.delete_all!(query: ms)
441+
442+
# Count how many cache entries reference a key
443+
ms = keyref_match_spec(:product_456)
444+
count = MyCache.count_all!(query: ms)
445+
446+
# Get all cache keys that reference a specific key
447+
ms = keyref_match_spec(:user_123)
448+
reference_keys = MyCache.get_all!(query: ms)
449+
450+
#### Example: Invalidating cached method results
451+
452+
When using the `:references` option with caching decorators, you can easily
453+
invalidate all cached results that depend on a specific entity:
454+
455+
defmodule MyApp.UserAccounts do
456+
use Nebulex.Caching, cache: MyApp.Cache
457+
use Nebulex.Adapters.Local.QueryHelper
458+
459+
@decorate cacheable(key: id)
460+
def get_user_account(id) do
461+
# your logic ...
462+
end
463+
464+
@decorate cacheable(key: email, references: &(&1 && &1.id))
465+
def get_user_account_by_email(email) do
466+
# your logic ...
467+
end
468+
469+
@decorate cacheable(key: token, references: &(&1 && &1.id))
470+
def get_user_account_by_token(token) do
471+
# your logic ...
472+
end
473+
474+
@decorate cache_evict(key: user.id, query: &__MODULE__.keyref_query/1)
475+
def update_user_account(user, attrs) do
476+
# your logic ...
477+
end
478+
479+
def keyref_query(%{args: [user | _]} = _context) do
480+
keyref_match_spec(user.id)
481+
end
482+
end
483+
end
484+
485+
See `Nebulex.Adapters.Local.QueryHelper.keyref_match_spec/2` for more details.
486+
487+
---
488+
416489
See `Nebulex.Adapters.Local.QueryHelper` for complete documentation.
417490
418491
## Tagging entries

lib/nebulex/adapters/local/query_helper.ex

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@ if Code.ensure_loaded?(Ex2ms) do
8484
it.
8585
"""
8686

87+
import Ex2ms
88+
89+
## API
90+
8791
@doc """
8892
Imports the query helper macros and `Ex2ms` for building match specifications.
8993
@@ -245,5 +249,77 @@ if Code.ensure_loaded?(Ex2ms) do
245249
# Return as a tuple AST node
246250
{:{}, [], elements}
247251
end
252+
253+
@doc """
254+
Builds a match spec for finding cache reference entries (keyrefs).
255+
256+
This is useful for invalidating all reference keys that point to a specific
257+
referenced key. Cache references are created using the `keyref/2` function
258+
in Nebulex caching decorators.
259+
260+
The match spec returns the reference key (the cache key that points to the
261+
referenced key). This works seamlessly with `get_all/1`, `delete_all/1`, and
262+
`count_all/1` operations.
263+
264+
## Parameters
265+
266+
* `referenced_key` - The key that references point to (required).
267+
* `opts` - Optional keyword list:
268+
* `:cache` - Filter references to a specific cache (optional). When not
269+
provided, matches references in any cache (including `nil` for local
270+
references).
271+
272+
## Examples
273+
274+
# Delete all reference entries pointing to a specific key (any cache)
275+
ms = keyref_match_spec(:user_123)
276+
MyCache.delete_all!(query: ms)
277+
278+
# Delete reference entries pointing to a key in a specific cache
279+
ms = keyref_match_spec(:user_123, cache: MyApp.UserCache)
280+
MyCache.delete_all!(query: ms)
281+
282+
# Count how many references point to a key
283+
ms = keyref_match_spec(:product_456)
284+
MyCache.count_all!(query: ms)
285+
286+
# Get all cache keys that are references to a specific key
287+
ms = keyref_match_spec(:user_123)
288+
reference_keys = MyCache.get_all!(query: ms)
289+
290+
## Background
291+
292+
When using the `:references` option with caching decorators, Nebulex stores
293+
reference entries as keyref records with the structure:
294+
`{:"$nbx_keyref_spec", cache, key, ttl}`. This function helps you query and
295+
clean up these reference entries.
296+
297+
## See also
298+
299+
* `Nebulex.Caching` - For information about cache references and the
300+
`:references` option.
301+
302+
"""
303+
@spec keyref_match_spec(term(), keyword()) :: :ets.match_spec()
304+
def keyref_match_spec(referenced_key, opts \\ []) do
305+
{cache_filter, _opts} = Keyword.pop(opts, :cache)
306+
307+
# Always return the reference key (the cache key that points to the referenced key)
308+
# This works for get_all (returns the keys), and the adapter overrides it for
309+
# delete_all/count_all to return true as needed
310+
if cache_filter do
311+
fun do
312+
{:entry, k, {:"$nbx_keyref_spec", c, ref_k, _t}, _, _, _}
313+
when ref_k == ^referenced_key and c == ^cache_filter ->
314+
k
315+
end
316+
else
317+
fun do
318+
{:entry, k, {:"$nbx_keyref_spec", _c, ref_k, _t}, _, _, _}
319+
when ref_k == ^referenced_key ->
320+
k
321+
end
322+
end
323+
end
248324
end
249325
end

test/nebulex/adapters/local/query_helper_test.exs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,4 +295,93 @@ defmodule Nebulex.Adapters.Local.QueryHelperTest do
295295
assert Enum.sort(result) == [{4, 100}, {5, 200}]
296296
end
297297
end
298+
299+
describe "keyref_match_spec/2" do
300+
test "returns a valid match spec" do
301+
ms = keyref_match_spec(:user_123)
302+
303+
# Should return a valid ETS match spec (list of tuples)
304+
assert is_list(ms)
305+
assert length(ms) == 1
306+
assert {_pattern, _guards, _select} = hd(ms)
307+
end
308+
309+
test "returns a valid match spec with cache filter" do
310+
ms = keyref_match_spec(:user_123, cache: MyApp.Cache)
311+
312+
assert is_list(ms)
313+
assert length(ms) == 1
314+
end
315+
end
316+
317+
describe "keyref_match_spec/2 integration with ETS" do
318+
setup do
319+
table_name = :"test_keyref_table_#{:erlang.unique_integer([:positive])}"
320+
table = :ets.new(table_name, [:set, :public, :named_table, keypos: 2])
321+
322+
# Insert some regular entries
323+
true = :ets.insert(table, {:entry, :key1, "value1", 1000, 2000, nil})
324+
true = :ets.insert(table, {:entry, :key2, "value2", 1100, 2100, nil})
325+
326+
# Insert keyref entries (cache references)
327+
# Reference to :user_123 in nil cache (local reference)
328+
true =
329+
:ets.insert(
330+
table,
331+
{:entry, :ref1, {:"$nbx_keyref_spec", nil, :user_123, nil}, 1200, :infinity, nil}
332+
)
333+
334+
# Reference to :user_123 in MyApp.Cache
335+
true =
336+
:ets.insert(
337+
table,
338+
{:entry, :ref2, {:"$nbx_keyref_spec", MyApp.Cache, :user_123, nil}, 1300, :infinity, nil}
339+
)
340+
341+
# Reference to :user_456 in MyApp.Cache
342+
true =
343+
:ets.insert(
344+
table,
345+
{:entry, :ref3, {:"$nbx_keyref_spec", MyApp.Cache, :user_456, nil}, 1400, :infinity, nil}
346+
)
347+
348+
# Reference to :user_123 in AnotherCache
349+
true =
350+
:ets.insert(
351+
table,
352+
{:entry, :ref4, {:"$nbx_keyref_spec", AnotherCache, :user_123, nil}, 1500, :infinity, nil}
353+
)
354+
355+
on_exit(fn ->
356+
if :ets.whereis(table_name) != :undefined do
357+
:ets.delete(table)
358+
end
359+
end)
360+
361+
%{table: table}
362+
end
363+
364+
test "gets all reference keys pointing to a specific key (any cache)", %{table: table} do
365+
ms = keyref_match_spec(:user_123)
366+
result = :ets.select(table, ms) |> Enum.sort()
367+
368+
# Should return the keys of all reference entries
369+
assert result == [:ref1, :ref2, :ref4]
370+
end
371+
372+
test "gets reference keys for a specific cache", %{table: table} do
373+
ms = keyref_match_spec(:user_123, cache: MyApp.Cache)
374+
result = :ets.select(table, ms)
375+
376+
# Should return only :ref2 (reference to :user_123 in MyApp.Cache)
377+
assert result == [:ref2]
378+
end
379+
380+
test "returns empty when no references exist", %{table: table} do
381+
ms = keyref_match_spec(:nonexistent_key)
382+
result = :ets.select(table, ms)
383+
384+
assert result == []
385+
end
386+
end
298387
end

test/shared/local_test_case.exs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,19 @@ defmodule Nebulex.Adapters.LocalTest do
480480
ms = match_spec value: v, tag: t, where: is_binary(v) and t == :strings
481481
assert cache.count_all!(query: ms) == 2
482482
end
483+
484+
test "QueryHelper: query a map as a value", %{cache: cache} do
485+
assert cache.put_all([a: %{x: 1}, b: %{x: 2}], tag: :maps) == :ok
486+
487+
var = 1
488+
ms = match_spec value: %{x: x}, tag: t, where: x == ^var and t == :maps, select: x
489+
490+
assert cache.get_all!(query: ms) == [1]
491+
492+
ms = match_spec value: %{x: x}, tag: t, where: t == :maps, select: x
493+
494+
assert cache.get_all!(query: ms) |> Enum.sort() == [1, 2]
495+
end
483496
end
484497

485498
describe "older generation hitted on" do

0 commit comments

Comments
 (0)