Skip to content

Commit d72f87a

Browse files
committed
Overall enhancements
1 parent 396d397 commit d72f87a

File tree

7 files changed

+508
-28
lines changed

7 files changed

+508
-28
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Add `:nebulex_local` to your list of dependencies in `mix.exs`:
1515
```elixir
1616
def deps do
1717
[
18-
{:nebulex_local, "~> 3.0.0-rc.1"}
18+
{:nebulex_local, "~> 3.0.0-rc.2"}
1919
]
2020
end
2121
```

lib/nebulex/adapters/local.ex

Lines changed: 151 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ defmodule Nebulex.Adapters.Local do
2323
* Expiration - A status based on TTL (Time To Live) option. To maintain
2424
cache performance, expired entries may not be immediately removed or
2525
evicted, they are expired or evicted on-demand, when the key is read.
26-
* Eviction - [Generational Garbage Collection][gc].
26+
* Eviction - Generational Garbage Collection
27+
(see `Nebulex.Adapters.Local.Generation`).
2728
* Sharding - For intensive workloads, the Cache may also be partitioned
2829
(by using `:shards` backend and specifying the `:partitions` option).
2930
* Support for transactions via Erlang global name registration facility.
@@ -32,8 +33,6 @@ defmodule Nebulex.Adapters.Local do
3233
* Automatic retry logic for handling race conditions during garbage
3334
collection (see [Concurrency and resilience](#module-concurrency-and-resilience)).
3435
35-
[gc]: http://hexdocs.pm/nebulex/3.0.0-rc.1/Nebulex.Adapters.Local.Generation.html
36-
3736
## Concurrency and resilience
3837
3938
The local adapter implements automatic retry logic to handle race conditions
@@ -482,11 +481,7 @@ defmodule Nebulex.Adapters.Local do
482481
end
483482
end
484483
485-
See `Nebulex.Adapters.Local.QueryHelper.keyref_match_spec/2` for more details.
486-
487-
---
488-
489-
See `Nebulex.Adapters.Local.QueryHelper` for complete documentation.
484+
> See `Nebulex.Adapters.Local.QueryHelper` for complete documentation.
490485
491486
## Tagging entries
492487
@@ -509,10 +504,13 @@ defmodule Nebulex.Adapters.Local do
509504
MyCache.put("user:123:profile", user_data, tag: :user_123)
510505
511506
# Tag multiple entries at once
512-
MyCache.put_all([
513-
{"session:abc:data", session_data},
514-
{"session:abc:prefs", preferences}
515-
], tag: :session_abc)
507+
MyCache.put_all(
508+
[
509+
{"session:abc:data", session_data},
510+
{"session:abc:prefs", preferences},
511+
],
512+
tag: :session_abc
513+
)
516514
517515
# Different tags for different entry groups
518516
MyCache.put_all([a: 1, b: 2, c: 3], tag: :group_a)
@@ -604,6 +602,147 @@ defmodule Nebulex.Adapters.Local do
604602
Tags can be any Elixir term (atoms, tuples, strings, etc.), giving you
605603
flexibility in how you organize your cache entries.
606604
605+
## Using Caching Decorators with QueryHelper
606+
607+
The `Nebulex.Caching` decorators (`@decorate`) integrate seamlessly with
608+
QueryHelper and tagging for powerful cache management patterns. This section
609+
shows practical examples of combining decorators with QueryHelper and tags.
610+
611+
### Entry Tagging with Decorators
612+
613+
You can tag entries automatically when using `@decorate cacheable` and
614+
`@decorate cache_put` by specifying the `:tag` option:
615+
616+
defmodule MyApp.UserCache do
617+
use Nebulex.Caching, cache: MyApp.Cache
618+
use Nebulex.Adapters.Local.QueryHelper
619+
620+
# Cache user data with automatic tagging
621+
@decorate cacheable(key: user_id, opts: [tag: :users])
622+
def get_user(user_id) do
623+
# fetch user from database
624+
{:ok, user}
625+
end
626+
627+
# Cache user permissions with automatic tagging
628+
@decorate cacheable(key: user_id, opts: [tag: :permissions])
629+
def get_user_permissions(user_id) do
630+
# fetch permissions from database
631+
{:ok, permissions}
632+
end
633+
634+
# Store session data with automatic tagging
635+
@decorate cache_put(key: session_id, opts: [tag: :sessions])
636+
def create_session(session_id, data) do
637+
data
638+
end
639+
end
640+
641+
### Selective Cache Invalidation with Tags
642+
643+
Use the `@decorate cache_evict` decorator with QueryHelper to invalidate
644+
entries by tag. This is useful for clearing related cached data:
645+
646+
defmodule MyApp.UserCache do
647+
use Nebulex.Caching, cache: MyApp.Cache
648+
use Nebulex.Adapters.Local.QueryHelper
649+
650+
# ... cacheable functions as above ...
651+
652+
# Evict all cached data for a specific user
653+
@decorate cache_evict(query: &evict_user_query/1)
654+
def invalidate_user(user_id) do
655+
:ok
656+
end
657+
658+
defp evict_user_query(context) do
659+
# Return a QueryHelper match spec to evict entries by tag
660+
match_spec tag: t, where: t == :users, select: true
661+
end
662+
end
663+
664+
### Cache Reference Invalidation with keyref_match_spec
665+
666+
When using the `:references` option to track cache dependencies, you can
667+
use `keyref_match_spec` with `cache_evict` to invalidate all dependent
668+
entries:
669+
670+
defmodule MyApp.UserAccounts do
671+
use Nebulex.Caching, cache: MyApp.Cache
672+
use Nebulex.Adapters.Local.QueryHelper
673+
674+
# Cache user account by ID
675+
@decorate cacheable(key: user_id)
676+
def get_user_account(user_id) do
677+
fetch_user(user_id)
678+
end
679+
680+
# Cache user account by email, referencing the user ID
681+
@decorate cacheable(key: email, references: &(&1 && &1.id))
682+
def get_user_account_by_email(email) do
683+
user = fetch_user_by_email(email)
684+
{:ok, user}
685+
end
686+
687+
# Cache user account by token, also referencing the user ID
688+
@decorate cacheable(key: token, references: &(&1 && &1.id))
689+
def get_user_account_by_token(token) do
690+
user = fetch_user_by_token(token)
691+
{:ok, user}
692+
end
693+
694+
# Evict all cache entries referencing a specific user
695+
# This invalidates all lookups (by id, email, token) in one operation
696+
@decorate cache_evict(key: user_id, query: &invalidate_refs/1)
697+
def update_user_account(user_id, attrs) do
698+
:ok
699+
end
700+
701+
defp invalidate_refs(%{args: [user_id | _]} = _context) do
702+
keyref_match_spec(user_id)
703+
end
704+
end
705+
706+
### Practical Pattern: Clearing All Session Data
707+
708+
Here's a complete example showing how to manage user sessions with automatic
709+
tagging and selective eviction:
710+
711+
defmodule MyApp.Sessions do
712+
use Nebulex.Caching, cache: MyApp.Cache
713+
use Nebulex.Adapters.Local.QueryHelper
714+
715+
# Store session data with automatic tagging by user ID
716+
@decorate cache_put(key: session_id, opts: [tag: {:session, user_id}])
717+
def store_session(user_id, session_id, data) do
718+
data
719+
end
720+
721+
# Evict all sessions for a specific user when they log out
722+
@decorate cache_evict(query: &evict_user_sessions_query/1)
723+
def logout_user(user_id) do
724+
:ok
725+
end
726+
727+
defp evict_user_sessions_query(%{args: [user_id]} = _context) do
728+
match_spec tag: t, where: t == {:session, user_id}
729+
end
730+
731+
# Clear all sessions across all users (e.g., during maintenance)
732+
@decorate cache_evict(query: &evict_all_sessions_query/1)
733+
def clear_all_sessions do
734+
:ok
735+
end
736+
737+
defp evict_all_sessions_query(_context) do
738+
# Match all entries with a session tag (pattern {:session, _})
739+
match_spec tag: t, where: is_tuple(t) and elem(t, 0) == :session
740+
end
741+
end
742+
743+
> The combination of decorators, QueryHelper, and tagging provides a clean,
744+
> declarative way to manage cache lifecycles with minimal boilerplate.
745+
607746
## Transaction API
608747
609748
This adapter inherits the default implementation provided by

lib/nebulex/adapters/local/query_helper.ex

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -240,10 +240,7 @@ if Code.ensure_loaded?(Ex2ms) do
240240

241241
# Build the tuple elements
242242
elements = [
243-
:entry
244-
| Enum.map(ordered_fields, fn field ->
245-
Map.get(field_map, field, {:_, [], Elixir})
246-
end)
243+
:entry | Enum.map(ordered_fields, &Map.get(field_map, &1, {:_, [], Elixir}))
247244
]
248245

249246
# Return as a tuple AST node

mix.exs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ defmodule NebulexAdaptersLocal.MixProject do
22
use Mix.Project
33

44
@source_url "http://github.com/elixir-nebulex/nebulex_local"
5-
@version "3.0.0-rc.1"
6-
# @nbx_tag "3.0.0-rc.1"
7-
# @nbx_vsn "3.0.0-rc.1"
5+
@version "3.0.0-rc.2"
6+
# @nbx_tag "3.0.0-rc.2"
7+
# @nbx_vsn "3.0.0-rc.2"
88

99
def project do
1010
[
@@ -56,6 +56,7 @@ defmodule NebulexAdaptersLocal.MixProject do
5656
{:sobelow, "~> 0.14", only: [:dev, :test], runtime: false},
5757
{:stream_data, "~> 1.2", only: [:dev, :test]},
5858
{:mimic, "~> 2.1", only: :test},
59+
{:decorator, "~> 1.4", only: :test},
5960

6061
# Benchmark Test
6162
{:benchee, "~> 1.4", only: [:dev, :test]},

mix.lock

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
%{
2-
"benchee": {:hex, :benchee, "1.4.0", "9f1f96a30ac80bab94faad644b39a9031d5632e517416a8ab0a6b0ac4df124ce", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "299cd10dd8ce51c9ea3ddb74bb150f93d25e968f93e4c1fa31698a8e4fa5d715"},
2+
"benchee": {:hex, :benchee, "1.5.0", "4d812c31d54b0ec0167e91278e7de3f596324a78a096fd3d0bea68bb0c513b10", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.1", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "5b075393aea81b8ae74eadd1c28b1d87e8a63696c649d8293db7c4df3eb67535"},
33
"benchee_html": {:hex, :benchee_html, "1.0.1", "1e247c0886c3fdb0d3f4b184b653a8d6fb96e4ad0d0389267fe4f36968772e24", [:mix], [{:benchee, ">= 0.99.0 and < 2.0.0", [hex: :benchee, repo: "hexpm", optional: false]}, {:benchee_json, "~> 1.0", [hex: :benchee_json, repo: "hexpm", optional: false]}], "hexpm", "b00a181af7152431901e08f3fc9f7197ed43ff50421a8347b0c80bf45d5b3fef"},
44
"benchee_json": {:hex, :benchee_json, "1.0.0", "cc661f4454d5995c08fe10dd1f2f72f229c8f0fb1c96f6b327a8c8fc96a91fe5", [:mix], [{:benchee, ">= 0.99.0 and < 2.0.0", [hex: :benchee, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "da05d813f9123505f870344d68fb7c86a4f0f9074df7d7b7e2bb011a63ec231c"},
55
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
66
"credo": {:hex, :credo, "1.7.13", "126a0697df6b7b71cd18c81bc92335297839a806b6f62b61d417500d1070ff4e", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "47641e6d2bbff1e241e87695b29f617f1a8f912adea34296fb10ecc3d7e9e84f"},
7+
"decorator": {:hex, :decorator, "1.4.0", "a57ac32c823ea7e4e67f5af56412d12b33274661bb7640ec7fc882f8d23ac419", [:mix], [], "hexpm", "0a07cedd9083da875c7418dea95b78361197cf2bf3211d743f6f7ce39656597f"},
78
"deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"},
89
"dialyxir": {:hex, :dialyxir, "1.4.6", "7cca478334bf8307e968664343cbdb432ee95b4b68a9cba95bdabb0ad5bdfd9a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "8cf5615c5cd4c2da6c501faae642839c8405b49f8aa057ad4ae401cb808ef64d"},
910
"earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"},
1011
"erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"},
1112
"ex2ms": {:hex, :ex2ms, "1.7.0", "45b9f523d0b777667ded60070d82d871a37e294f0b6c5b8eca86771f00f82ee1", [:mix], [], "hexpm", "2589eee51f81f1b1caa6d08c990b1ad409215fe6f64c73f73c67d36ed10be827"},
12-
"ex_doc": {:hex, :ex_doc, "0.38.4", "ab48dff7a8af84226bf23baddcdda329f467255d924380a0cf0cee97bb9a9ede", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "f7b62346408a83911c2580154e35613eb314e0278aeea72ed7fedef9c1f165b2"},
13+
"ex_doc": {:hex, :ex_doc, "0.39.1", "e19d356a1ba1e8f8cfc79ce1c3f83884b6abfcb79329d435d4bbb3e97ccc286e", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "8abf0ed3e3ca87c0847dfc4168ceab5bedfe881692f1b7c45f4a11b232806865"},
1314
"excoveralls": {:hex, :excoveralls, "0.18.5", "e229d0a65982613332ec30f07940038fe451a2e5b29bce2a5022165f0c9b157e", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "523fe8a15603f86d64852aab2abe8ddbd78e68579c8525ae765facc5eae01562"},
1415
"file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"},
1516
"ham": {:hex, :ham, "0.3.2", "02ae195f49970ef667faf9d01bc454fb80909a83d6c775bcac724ca567aeb7b3", [:mix], [], "hexpm", "b71cc684c0e5a3d32b5f94b186770551509e93a9ae44ca1c1a313700f2f6a69a"},
@@ -18,7 +19,7 @@
1819
"makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"},
1920
"makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"},
2021
"mimic": {:hex, :mimic, "2.1.1", "29008b71c842b652b065d6f9a24e05d84a2fac7181c34627e1ef5229659702e1", [:mix], [{:ham, "~> 0.3", [hex: :ham, repo: "hexpm", optional: false]}], "hexpm", "a3c330c8840feb29ab43b2375ac023073b936429e5320dd5ca1c95a7322a0da7"},
21-
"nebulex": {:git, "https://github.com/elixir-nebulex/nebulex.git", "4ecbc0ebebfb16d2135f457237eadf2d47c90b38", [branch: "main"]},
22+
"nebulex": {:git, "https://github.com/elixir-nebulex/nebulex.git", "9f201516c5f66065481fae4a5793233b6c64d7eb", [branch: "main"]},
2223
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
2324
"nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
2425
"shards": {:hex, :shards, "1.1.1", "8b42323457d185b26b15d05187784ce6c5d1e181b35c46fca36c45f661defe02", [:make, :rebar3], [], "hexpm", "169a045dae6668cda15fbf86d31bf433d0dbbaec42c8c23ca4f8f2d405ea8eda"},

test/nebulex/adapters/local/query_helper_test.exs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -188,9 +188,10 @@ defmodule Nebulex.Adapters.Local.QueryHelperTest do
188188
true = :ets.insert(table, {:entry, 3, "value3", 1200, :infinity, :tag_a})
189189
true = :ets.insert(table, {:entry, 4, 100, 1300, 2300, :tag_c})
190190
true = :ets.insert(table, {:entry, 5, 200, 1400, 2400, :tag_c})
191+
true = :ets.insert(table, {:entry, {:key, 6}, 300, 1500, 2500, :tag_d})
191192

192193
# Verify all entries were inserted
193-
5 = :ets.info(table, :size)
194+
assert :ets.info(table, :size) == 6
194195

195196
on_exit(fn ->
196197
if :ets.whereis(table_name) != :undefined do
@@ -217,7 +218,7 @@ defmodule Nebulex.Adapters.Local.QueryHelperTest do
217218
test "selects entries with value guard", %{table: table} do
218219
ms = match_spec key: k, value: v, where: is_integer(v) and v > 100, select: {k, v}
219220

220-
assert :ets.select(table, ms) == [{5, 200}]
221+
assert :ets.select(table, ms) |> Enum.sort() == [{5, 200}, {{:key, 6}, 300}] |> Enum.sort()
221222
end
222223

223224
test "selects entries with exp guard", %{table: table} do
@@ -245,7 +246,8 @@ defmodule Nebulex.Adapters.Local.QueryHelperTest do
245246
assert Enum.sort(result) ==
246247
Enum.sort([
247248
{:entry, 4, 100, 1300, 2300, :tag_c},
248-
{:entry, 5, 200, 1400, 2400, :tag_c}
249+
{:entry, 5, 200, 1400, 2400, :tag_c},
250+
{:entry, {:key, 6}, 300, 1500, 2500, :tag_d}
249251
])
250252
end
251253

@@ -265,14 +267,14 @@ defmodule Nebulex.Adapters.Local.QueryHelperTest do
265267
# Verify the entry was deleted
266268
assert :ets.lookup(table, 2) == []
267269
# Other entries should still exist
268-
assert :ets.info(table, :size) == 4
270+
assert :ets.info(table, :size) == 5
269271
end
270272

271273
test "matches without guards", %{table: table} do
272274
ms = match_spec key: k, value: v, select: k
273275
result = :ets.select(table, ms)
274276

275-
assert Enum.sort(result) == [1, 2, 3, 4, 5]
277+
assert Enum.sort(result) |> Enum.sort() == [1, 2, 3, 4, 5, {:key, 6}] |> Enum.sort()
276278
end
277279

278280
test "matches with only specific field binding", %{table: table} do
@@ -294,6 +296,12 @@ defmodule Nebulex.Adapters.Local.QueryHelperTest do
294296

295297
assert Enum.sort(result) == [{4, 100}, {5, 200}]
296298
end
299+
300+
test "selects entries with key as tuple", %{table: table} do
301+
ms = match_spec key: k, value: v, where: k == {:key, 6}, select: {k, v}
302+
303+
assert :ets.select(table, ms) == [{{:key, 6}, 300}]
304+
end
297305
end
298306

299307
describe "keyref_match_spec/2" do

0 commit comments

Comments
 (0)