Skip to content

Commit 8c20ea4

Browse files
Qwerty5Uiopvbaranov
andcommitted
chore: Distributed MapCache (blockscout#14411)
Co-authored-by: Victor Baranov <baranov.viktor.27@gmail.com>
1 parent 596de60 commit 8c20ea4

4 files changed

Lines changed: 82 additions & 11 deletions

File tree

apps/explorer/lib/explorer/chain/map_cache.ex

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ defmodule Explorer.Chain.MapCache do
4545
- `{:return, value}` that will cause the value to be returned but not stored
4646
This allows to define of a default value or perform some actions.
4747
By default it will simply `{:return, nil}`
48+
49+
## Distributed writes
50+
51+
In split API/indexer deployments, `set/2` and `update/2` use `do_raw/2`: the write runs on
52+
the local `ConCache` first, then indexer nodes multicast the same operation to other cluster
53+
nodes via `:erpc` (`propagate: false` on receivers). Reads are not distributed.
4854
"""
4955

5056
@type key :: atom()
@@ -114,6 +120,7 @@ defmodule Explorer.Chain.MapCache do
114120

115121
# credo:disable-for-next-line Credo.Check.Refactor.LongQuoteBlocks
116122
quote do
123+
require Logger
117124
alias Explorer.Chain.MapCache
118125

119126
@behaviour MapCache
@@ -156,7 +163,7 @@ defmodule Explorer.Chain.MapCache do
156163

157164
@impl MapCache
158165
def set(key, value) do
159-
ConCache.put(cache_name(), key, value)
166+
do_raw(fn -> ConCache.put(cache_name(), key, value) end)
160167
end
161168

162169
@impl MapCache
@@ -169,7 +176,31 @@ defmodule Explorer.Chain.MapCache do
169176

170177
@impl MapCache
171178
def update(key, value) do
172-
ConCache.update(cache_name(), key, fn old_val -> handle_update(key, old_val, value) end)
179+
do_raw(fn -> ConCache.update(cache_name(), key, fn old_val -> handle_update(key, old_val, value) end) end)
180+
end
181+
182+
@doc """
183+
Applies a ConCache write locally, then propagates it from indexer nodes.
184+
185+
With the default `propagate: true`, the given zero-arity function runs on this node first.
186+
When `Explorer.mode/0` is `:indexer`, the same function is multicast to `Node.list/0` with
187+
`propagate: false` so API nodes apply the write without re-propagating.
188+
"""
189+
def do_raw(function, propagate \\ true) do
190+
function.()
191+
192+
case Explorer.mode() do
193+
:indexer ->
194+
if propagate do
195+
Node.list() |> :erpc.multicast(__MODULE__, :do_raw, [function, false])
196+
else
197+
Logger.error("[#{__MODULE__}] Indexer got unexpected propagation call to do_raw/2")
198+
:ok
199+
end
200+
201+
_ ->
202+
:ok
203+
end
173204
end
174205

175206
### Autogenerated named functions

apps/explorer/lib/explorer/chain/ordered_cache.ex

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ defmodule Explorer.Chain.OrderedCache do
3636
and `c:element_to_id/1` callbacks.
3737
For typechecking purposes it's also recommended to override the `t:element/0`
3838
and `t:id/0` type definitions.
39+
40+
## Distributed writes
41+
42+
In split API/indexer deployments, `update/1` uses `do_raw_update/2`: the ids list and
43+
elements are written to the local `ConCache` first, then indexer nodes multicast the prepared
44+
update to other cluster nodes via `:erpc` (`propagate: false` on receivers).
3945
"""
4046

4147
@type element :: struct()
@@ -290,18 +296,24 @@ defmodule Explorer.Chain.OrderedCache do
290296

291297
def update(element), do: update([element])
292298

299+
@doc """
300+
Merges prepared elements into the local ordered cache, then propagates from indexer nodes.
301+
302+
Always updates the local ids list and element entries first. When `Explorer.mode/0` is
303+
`:indexer` and `propagate` is `true`, multicasts the same prepared elements to `Node.list/0`
304+
with `propagate: false` so API nodes apply the write without re-propagating.
305+
"""
293306
def do_raw_update(prepared_elements, propagate) do
294-
case Explorer.mode() do
295-
mode when mode in [:all, :api] ->
296-
ConCache.update(cache_name(), ids_list_key(), fn ids ->
297-
updated_list =
298-
prepared_elements
299-
|> merge_and_update(ids || [], max_size())
307+
ConCache.update(cache_name(), ids_list_key(), fn ids ->
308+
updated_list =
309+
prepared_elements
310+
|> merge_and_update(ids || [], max_size())
300311

301-
# ids_list is set to never expire
302-
{:ok, %ConCache.Item{value: updated_list, ttl: :infinity}}
303-
end)
312+
# ids_list is set to never expire
313+
{:ok, %ConCache.Item{value: updated_list, ttl: :infinity}}
314+
end)
304315

316+
case Explorer.mode() do
305317
:indexer ->
306318
if propagate do
307319
Node.list() |> :erpc.multicast(__MODULE__, :do_raw_update, [prepared_elements, false])

apps/explorer/test/explorer/chain/cache/transactions_test.exs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,33 @@ defmodule Explorer.Chain.Cache.TransactionsTest do
8585
end
8686
end
8787

88+
describe "indexer mode" do
89+
setup do
90+
old_mode = Application.get_env(:explorer, :mode)
91+
Application.put_env(:explorer, :mode, :indexer)
92+
93+
Supervisor.terminate_child(Explorer.Supervisor, Transactions.child_id())
94+
Supervisor.restart_child(Explorer.Supervisor, Transactions.child_id())
95+
96+
on_exit(fn -> Application.put_env(:explorer, :mode, old_mode) end)
97+
98+
:ok
99+
end
100+
101+
test "update/1 writes to local ConCache without connected API nodes" do
102+
assert Explorer.mode() == :indexer
103+
# Single-node test env: propagation multicasts to an empty list, so only the
104+
# local-first write in do_raw_update/2 populates the cache.
105+
assert Node.list() == []
106+
107+
transaction = insert(:transaction) |> preload_all()
108+
109+
Transactions.update(transaction)
110+
111+
assert Transactions.take(1) == [transaction]
112+
end
113+
end
114+
88115
defp preload_all(transactions) when is_list(transactions) do
89116
Enum.map(transactions, &preload_all(&1))
90117
end

cspell.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,7 @@
450450
"mtransfer",
451451
"mult",
452452
"multicall",
453+
"multicasts",
453454
"multichain",
454455
"multiprotocol",
455456
"multis",

0 commit comments

Comments
 (0)