Skip to content

Commit 7cbcc1e

Browse files
committed
[#249] Remove default transaction implementation from core library
1 parent 92573ec commit 7cbcc1e

File tree

10 files changed

+228
-294
lines changed

10 files changed

+228
-294
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,18 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
2020
defaults.
2121
[#248](https://github.com/elixir-nebulex/nebulex/issues/248).
2222

23+
### Backwards incompatible changes
24+
25+
- [Nebulex.Adapter.Transaction] The default transaction implementation has been
26+
removed from the core Nebulex library. Each adapter is now responsible for
27+
providing its own transaction implementation tailored to its specific needs.
28+
This change keeps the core library more generic and allows adapters to
29+
optimize transaction handling for their particular use cases (e.g., local
30+
ETS-based locking for single-node caches vs. distributed locking for
31+
multi-node setups). Adapters that support transactions must implement
32+
the `Nebulex.Adapter.Transaction` behaviour independently.
33+
[#249](https://github.com/elixir-nebulex/nebulex/issues/249).
34+
2335
### Enhancements
2436

2537
## [v3.0.0-rc.2](https://github.com/elixir-nebulex/nebulex/tree/v3.0.0-rc.2) (2025-12-07)

lib/nebulex/adapter/transaction.ex

Lines changed: 0 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,6 @@
11
defmodule Nebulex.Adapter.Transaction do
22
@moduledoc """
33
Specifies the adapter Transaction API.
4-
5-
## Default implementation
6-
7-
This module also provides a default implementation which uses the Erlang
8-
library `:global`.
9-
10-
This implementation accepts the following options:
11-
12-
#{Nebulex.Adapter.Transaction.Options.options_docs()}
13-
14-
Let's see an example:
15-
16-
MyCache.transaction fn ->
17-
counter = MyCache.get(:counter)
18-
MyCache.set(:counter, counter + 1)
19-
end
20-
21-
Locking only the involved key (recommended):
22-
23-
MyCache.transaction(
24-
fn ->
25-
counter = MyCache.get(:counter)
26-
MyCache.set(:counter, counter + 1)
27-
end,
28-
[keys: [:counter]]
29-
)
30-
31-
MyCache.transaction(
32-
fn ->
33-
alice = MyCache.get(:alice)
34-
bob = MyCache.get(:bob)
35-
MyCache.set(:alice, %{alice | balance: alice.balance + 100})
36-
MyCache.set(:bob, %{bob | balance: bob.balance + 100})
37-
end,
38-
[keys: [:alice, :bob]]
39-
)
40-
414
"""
425

436
@doc """
@@ -66,109 +29,4 @@ defmodule Nebulex.Adapter.Transaction do
6629
"""
6730
@callback in_transaction?(Nebulex.Adapter.adapter_meta(), Nebulex.Cache.opts()) ::
6831
Nebulex.Cache.ok_error_tuple(boolean())
69-
70-
alias Nebulex.Adapter.Transaction.Options
71-
72-
import Nebulex.Utils, only: [wrap_ok: 1, wrap_error: 2]
73-
74-
@doc false
75-
defmacro __using__(_opts) do
76-
quote do
77-
@behaviour Nebulex.Adapter.Transaction
78-
79-
@impl true
80-
defdelegate transaction(adapter_meta, fun, opts), to: unquote(__MODULE__)
81-
82-
@impl true
83-
defdelegate in_transaction?(adapter_meta, opts), to: unquote(__MODULE__)
84-
85-
defoverridable transaction: 3, in_transaction?: 2
86-
end
87-
end
88-
89-
@doc false
90-
def transaction(%{cache: cache, pid: pid} = adapter_meta, fun, opts) do
91-
opts = Options.validate!(opts)
92-
93-
adapter_meta
94-
|> do_in_transaction?()
95-
|> do_transaction(
96-
pid,
97-
adapter_meta[:name] || cache,
98-
Keyword.fetch!(opts, :keys),
99-
Keyword.get(opts, :nodes, [node()]),
100-
Keyword.fetch!(opts, :retries),
101-
fun
102-
)
103-
end
104-
105-
@doc false
106-
def in_transaction?(adapter_meta, _opts) do
107-
wrap_ok do_in_transaction?(adapter_meta)
108-
end
109-
110-
## Helpers
111-
112-
defp do_in_transaction?(%{pid: pid}) do
113-
!!Process.get({pid, self()})
114-
end
115-
116-
defp do_transaction(true, _pid, _name, _keys, _nodes, _retries, fun) do
117-
{:ok, fun.()}
118-
end
119-
120-
defp do_transaction(false, pid, name, keys, nodes, retries, fun) do
121-
ids = lock_ids(name, keys)
122-
123-
case set_locks(ids, nodes, retries) do
124-
true ->
125-
try do
126-
_ = Process.put({pid, self()}, %{keys: keys, nodes: nodes})
127-
128-
{:ok, fun.()}
129-
after
130-
_ = Process.delete({pid, self()})
131-
132-
del_locks(ids, nodes)
133-
end
134-
135-
false ->
136-
wrap_error Nebulex.Error,
137-
reason: :transaction_aborted,
138-
cache: name,
139-
nodes: nodes,
140-
cache: name
141-
end
142-
end
143-
144-
defp set_locks(ids, nodes, retries) do
145-
maybe_set_lock = fn id, {:ok, acc} ->
146-
case :global.set_lock(id, nodes, retries) do
147-
true -> {:cont, {:ok, [id | acc]}}
148-
false -> {:halt, {:error, acc}}
149-
end
150-
end
151-
152-
case Enum.reduce_while(ids, {:ok, []}, maybe_set_lock) do
153-
{:ok, _} ->
154-
true
155-
156-
{:error, locked_ids} ->
157-
:ok = del_locks(locked_ids, nodes)
158-
159-
false
160-
end
161-
end
162-
163-
defp del_locks(ids, nodes) do
164-
Enum.each(ids, &:global.del_lock(&1, nodes))
165-
end
166-
167-
defp lock_ids(name, []) do
168-
[{name, self()}]
169-
end
170-
171-
defp lock_ids(name, keys) do
172-
Enum.map(keys, &{{name, &1}, self()})
173-
end
17432
end

lib/nebulex/adapter/transaction/options.ex

Lines changed: 0 additions & 68 deletions
This file was deleted.

lib/nebulex/adapters/nil.ex

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,7 @@ defmodule Nebulex.Adapters.Nil do
7070
@behaviour Nebulex.Adapter
7171
@behaviour Nebulex.Adapter.KV
7272
@behaviour Nebulex.Adapter.Queryable
73-
74-
# Inherit default transaction implementation
75-
use Nebulex.Adapter.Transaction
73+
@behaviour Nebulex.Adapter.Transaction
7674

7775
# Inherit default info implementation
7876
use Nebulex.Adapters.Common.Info
@@ -169,6 +167,18 @@ defmodule Nebulex.Adapters.Nil do
169167
with_hooks(opts, {:ok, Stream.each([], & &1)})
170168
end
171169

170+
## Nebulex.Adapter.Transaction
171+
172+
@impl true
173+
def transaction(_, fun, opts) do
174+
with_hooks(opts, {:ok, fun.()})
175+
end
176+
177+
@impl true
178+
def in_transaction?(_, opts) do
179+
with_hooks(opts, {:ok, false})
180+
end
181+
172182
## Private functions
173183

174184
defp with_hooks([], result) do

test/nebulex/adapters/nil_test.exs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -108,12 +108,6 @@ defmodule Nebulex.Adapters.NilTest do
108108

109109
test "in_transaction?", %{cache: cache} do
110110
assert cache.in_transaction?() == {:ok, false}
111-
112-
cache.transaction(fn ->
113-
:ok = cache.put(1, 11)
114-
115-
assert cache.in_transaction?() == {:ok, true}
116-
end)
117111
end
118112
end
119113

test/nebulex/cache_test.exs

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,23 @@ defmodule Nebulex.Adapters.CacheTest do
88

99
setup_with_dynamic_cache Nebulex.TestCache.Cache, __MODULE__
1010

11+
describe "error" do
12+
test "because cache is stopped", %{cache: cache, name: name} do
13+
:ok = stop_supervised!(name)
14+
15+
ops = [
16+
fn -> cache.put(1, 13) end,
17+
fn -> cache.put!(1, 13) end,
18+
fn -> cache.get!(1) end,
19+
fn -> cache.delete!(1) end
20+
]
21+
22+
for fun <- ops do
23+
assert_raise Nebulex.CacheNotFoundError, ~r/unable to find cache: #{inspect(name)}/, fun
24+
end
25+
end
26+
end
27+
1128
describe "KV:" do
1229
test "get_and_update", %{cache: cache} do
1330
fun = fn
@@ -176,19 +193,35 @@ defmodule Nebulex.Adapters.CacheTest do
176193
end
177194
end
178195

179-
describe "error" do
180-
test "because cache is stopped", %{cache: cache, name: name} do
181-
:ok = stop_supervised!(name)
196+
describe "transaction" do
197+
test "aborted", %{name: name, cache: cache} do
198+
key = {name, :aborted}
182199

183-
ops = [
184-
fn -> cache.put(1, 13) end,
185-
fn -> cache.put!(1, 13) end,
186-
fn -> cache.get!(1) end,
187-
fn -> cache.delete!(1) end
188-
]
200+
Task.start_link(fn ->
201+
_ = cache.put_dynamic_cache(name)
189202

190-
for fun <- ops do
191-
assert_raise Nebulex.CacheNotFoundError, ~r/unable to find cache: #{inspect(name)}/, fun
203+
cache.transaction(
204+
fn ->
205+
:ok = cache.put(key, true)
206+
207+
Process.sleep(1100)
208+
end,
209+
keys: [key],
210+
retries: 1
211+
)
212+
end)
213+
214+
:ok = Process.sleep(200)
215+
216+
assert_raise Nebulex.Error, ~r/transaction aborted\n\nError metadata:/, fn ->
217+
{:error, %Nebulex.Error{} = reason} =
218+
cache.transaction(
219+
fn -> cache.get(key) end,
220+
keys: [key],
221+
retries: 1
222+
)
223+
224+
raise reason
192225
end
193226
end
194227
end

0 commit comments

Comments
 (0)