Skip to content

Commit 2f565cf

Browse files
authored
feat: add mutations abstraction to deal with mnesia updates (#342)
The goal of this change is to be able to isolate mnesia updates into mutations which will all be ran in a single transaction and in the least amount of time possible. By making first-class mutations we can also debug easily what changes any given block or transaction makes. This also fixes random errors that come up inside mnesia transactions: mnesia transactions do not work properly when exceptions are thrown inside them, even if they are captured. closes #331
1 parent 9621d66 commit 2f565cf

17 files changed

+545
-180
lines changed

lib/ae_mdw/blocks.ex

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,11 @@ defmodule AeMdw.Blocks do
1313

1414
require Model
1515

16-
# This needs to be an actual type like AeMdw.Db.Tx.t()
17-
@type block :: term()
16+
@type height() :: non_neg_integer()
17+
@type mbi() :: non_neg_integer()
18+
@type time() :: non_neg_integer()
19+
@type block_index() :: {height(), mbi()}
20+
@type block :: map()
1821
@type cursor :: binary()
1922

2023
@typep direction :: Mnesia.direction()

lib/ae_mdw/contract.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ defmodule AeMdw.Contract do
1515
@tab __MODULE__
1616

1717
@type id :: binary()
18+
@type grouped_events() :: %{tx_hash() => [event()]}
1819

1920
@typep fname() :: binary()
2021
@typep tx_hash() :: binary()
@@ -326,7 +327,7 @@ defmodule AeMdw.Contract do
326327
end)
327328
end
328329

329-
@spec get_grouped_events(micro_block()) :: %{tx_hash() => [event()]}
330+
@spec get_grouped_events(micro_block()) :: grouped_events()
330331
def get_grouped_events(micro_block) do
331332
Enum.group_by(get_events(micro_block), fn {_event_name, %{tx_hash: tx_hash}} -> tx_hash end)
332333
end

lib/ae_mdw/db/model.ex

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ defmodule AeMdw.Db.Model do
22
@moduledoc """
33
Mnesia database model records.
44
"""
5+
alias AeMdw.Blocks
6+
alias AeMdw.Txs
7+
58
require Record
69
require Ex2ms
710

@@ -31,6 +34,14 @@ defmodule AeMdw.Db.Model do
3134
@tx_defaults [index: -1, id: <<>>, block_index: {-1, -1}, time: -1]
3235
defrecord :tx, @tx_defaults
3336

37+
@type tx ::
38+
record(:tx,
39+
index: Txs.txi(),
40+
id: Txs.tx_hash(),
41+
block_index: Blocks.block_index(),
42+
time: Blocks.time()
43+
)
44+
3445
# txs time index :
3546
# index = {mb_time_msecs (0..), tx_index = (0...)},
3647
@time_defaults [index: {-1, -1}, unused: nil]

lib/ae_mdw/db/mutation.ex

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
defprotocol AeMdw.Db.Mutation do
2+
@moduledoc """
3+
Protocol aimed at specifying all database mutations declaratively. This can be
4+
a simple write to mnesia, or more complex logic involving multiple mnesia
5+
queries or calls to other services.
6+
7+
The goal of this protocol is to do as much work as possible before starting
8+
the mnesia transaction. Once the mnesia transaction is started, the mutate
9+
function is called for every mutation generated.
10+
"""
11+
12+
@doc """
13+
Abstracted function that performs the actual mutation into the mnesia database.
14+
"""
15+
@spec mutate(t()) :: :ok
16+
def mutate(mutation)
17+
end
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
defmodule AeMdw.Db.Aex9AccountPresenceMutation do
2+
@moduledoc """
3+
Computes and derives Aex9 tokens, and stores it into the appropriate indexes.
4+
"""
5+
6+
alias AeMdw.Db.Sync
7+
alias AeMdw.Blocks
8+
9+
defstruct [:height, :mbi]
10+
11+
@opaque t() :: %__MODULE__{
12+
height: Blocks.height(),
13+
mbi: Blocks.mbi()
14+
}
15+
16+
@spec new(Blocks.height(), Blocks.mbi()) :: t()
17+
def new(height, mbi) do
18+
%__MODULE__{height: height, mbi: mbi}
19+
end
20+
21+
@spec mutate(t()) :: :ok
22+
def mutate(%__MODULE__{height: height, mbi: mbi}) do
23+
Sync.Contract.aex9_derive_account_presence!({height, mbi})
24+
end
25+
end
26+
27+
defimpl AeMdw.Db.Mutation, for: AeMdw.Db.Aex9AccountPresenceMutation do
28+
def mutate(mutation) do
29+
@for.mutate(mutation)
30+
end
31+
end
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
defmodule AeMdw.Db.ContractEventsMutation do
2+
@moduledoc """
3+
Stores the internal contract calls by using the contract events.
4+
"""
5+
6+
alias AeMdw.Contract
7+
alias AeMdw.Db.Contract, as: DBContract
8+
alias AeMdw.Db.Sync
9+
alias AeMdw.Txs
10+
11+
defstruct [:ct_pk, :events, :txi]
12+
13+
@typep event() :: Contract.event()
14+
@opaque t() :: %__MODULE__{
15+
ct_pk: DBContract.pubkey(),
16+
events: [event()],
17+
txi: Txs.txi()
18+
}
19+
20+
@spec new(DBContract.pubkey(), [event()], Txs.txi()) :: t()
21+
def new(ct_pk, events, txi) do
22+
%__MODULE__{ct_pk: ct_pk, events: events, txi: txi}
23+
end
24+
25+
@spec mutate(t()) :: :ok
26+
def mutate(%__MODULE__{ct_pk: ct_pk, events: events, txi: txi}) do
27+
ct_txi = Sync.Contract.get_txi(ct_pk)
28+
Sync.Contract.events(events, txi, ct_txi)
29+
end
30+
end
31+
32+
defimpl AeMdw.Db.Mutation, for: AeMdw.Db.ContractEventsMutation do
33+
def mutate(mutation) do
34+
@for.mutate(mutation)
35+
end
36+
end
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
defmodule AeMdw.Db.MnesiaWriteMutation do
2+
@moduledoc """
3+
This is the most basic kind of transaction, it just inserts a record in a
4+
mnesia table.
5+
"""
6+
7+
alias AeMdw.Mnesia
8+
9+
defstruct [:table, :record]
10+
11+
@opaque t() :: %__MODULE__{
12+
table: Mnesia.table(),
13+
record: Mnesia.record()
14+
}
15+
16+
@spec new(Mnesia.table(), Mnesia.record()) :: t()
17+
def new(table, record) do
18+
%__MODULE__{table: table, record: record}
19+
end
20+
21+
@spec mutate(t()) :: :ok
22+
def mutate(%__MODULE__{table: table, record: record}) do
23+
Mnesia.write(table, record)
24+
end
25+
end
26+
27+
defimpl AeMdw.Db.Mutation, for: AeMdw.Db.MnesiaWriteMutation do
28+
def mutate(mutation) do
29+
@for.mutate(mutation)
30+
end
31+
end
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
defmodule AeMdw.Db.WriteFieldsMutation do
2+
@moduledoc """
3+
Stores the indexes for the Fields table.
4+
"""
5+
6+
alias AeMdw.Blocks
7+
alias AeMdw.Node
8+
alias AeMdw.Db.Model
9+
alias AeMdw.Txs
10+
11+
require Model
12+
13+
defstruct [:type, :tx, :block_index, :txi]
14+
15+
@opaque t() :: %__MODULE__{
16+
type: Node.tx_type(),
17+
tx: Model.tx(),
18+
block_index: Blocks.block_index(),
19+
txi: Txs.txi()
20+
}
21+
22+
@spec new(Node.tx_type(), Model.tx(), Blocks.block_index(), Txs.txi()) :: t()
23+
def new(type, tx, block_index, txi) do
24+
%__MODULE__{
25+
type: type,
26+
tx: tx,
27+
block_index: block_index,
28+
txi: txi
29+
}
30+
end
31+
32+
@spec mutate(t()) :: :ok
33+
def mutate(%__MODULE__{type: tx_type, tx: tx, block_index: block_index, txi: txi}) do
34+
tx_type
35+
|> Node.tx_ids()
36+
|> Enum.each(fn {field, pos} ->
37+
<<_::256>> = pk = resolve_pubkey(elem(tx, pos), tx_type, field, block_index)
38+
write_field(tx_type, pos, pk, txi)
39+
end)
40+
end
41+
42+
defp write_field(tx_type, pos, pubkey, txi) do
43+
m_field = Model.field(index: {tx_type, pos, pubkey, txi})
44+
:mnesia.write(Model.Field, m_field, :write)
45+
Model.incr_count({tx_type, pos, pubkey})
46+
end
47+
48+
defp resolve_pubkey(id, :spend_tx, :recipient_id, block_index) do
49+
case :aeser_id.specialize(id) do
50+
{:name, name_hash} ->
51+
AeMdw.Db.Name.ptr_resolve!(block_index, name_hash, "account_pubkey")
52+
53+
{_tag, pk} ->
54+
pk
55+
end
56+
end
57+
58+
defp resolve_pubkey(id, _type, _field, _block_index) do
59+
{_tag, pk} = :aeser_id.specialize(id)
60+
pk
61+
end
62+
end
63+
64+
defimpl AeMdw.Db.Mutation, for: AeMdw.Db.WriteFieldsMutation do
65+
def mutate(mutation) do
66+
@for.mutate(mutation)
67+
end
68+
end
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
defmodule AeMdw.Db.WriteLinksMutation do
2+
@moduledoc """
3+
Creates all the necessary indexes for both Oracles and Names depending on the
4+
transaction type.
5+
"""
6+
7+
alias AeMdw.Blocks
8+
alias AeMdw.Db.Model
9+
alias AeMdw.Db.Sync
10+
alias AeMdw.Node
11+
alias AeMdw.Txs
12+
13+
require Model
14+
15+
defstruct [:type, :tx, :signed_tx, :txi, :tx_hash, :block_index]
16+
17+
@opaque t() :: %__MODULE__{
18+
type: Node.tx_type(),
19+
tx: Model.tx(),
20+
signed_tx: Node.signed_tx(),
21+
txi: Txs.txi(),
22+
tx_hash: Txs.tx_hash(),
23+
block_index: Blocks.block_index()
24+
}
25+
26+
@spec new(
27+
Node.tx_type(),
28+
Model.tx(),
29+
Node.signed_tx(),
30+
Txs.txi(),
31+
Txs.tx_hash(),
32+
Blocks.block_index()
33+
) :: t()
34+
def new(type, tx, signed_tx, txi, tx_hash, block_index) do
35+
%__MODULE__{
36+
type: type,
37+
tx: tx,
38+
signed_tx: signed_tx,
39+
txi: txi,
40+
tx_hash: tx_hash,
41+
block_index: block_index
42+
}
43+
end
44+
45+
@spec mutate(t()) :: :ok
46+
def mutate(%__MODULE__{
47+
type: :contract_create_tx,
48+
tx: tx,
49+
txi: txi,
50+
tx_hash: tx_hash,
51+
block_index: block_index
52+
}) do
53+
pk = :aect_contracts.pubkey(:aect_contracts.new(tx))
54+
owner_pk = :aect_create_tx.owner_pubkey(tx)
55+
:ets.insert(:ct_create_sync_cache, {pk, txi})
56+
write_origin(:contract_create_tx, pk, txi, tx_hash)
57+
Sync.Contract.create(pk, owner_pk, txi, block_index)
58+
end
59+
60+
def mutate(%__MODULE__{type: :contract_call_tx, tx: tx, txi: txi, block_index: block_index}) do
61+
pk = :aect_call_tx.contract_pubkey(tx)
62+
Sync.Contract.call(pk, tx, txi, block_index)
63+
end
64+
65+
def mutate(%__MODULE__{
66+
type: :channel_create_tx,
67+
signed_tx: signed_tx,
68+
txi: txi,
69+
tx_hash: tx_hash
70+
}) do
71+
{:ok, pk} = :aesc_utils.channel_pubkey(signed_tx)
72+
write_origin(:channel_create_tx, pk, txi, tx_hash)
73+
end
74+
75+
def mutate(%__MODULE__{
76+
type: :oracle_register_tx,
77+
tx: tx,
78+
txi: txi,
79+
tx_hash: tx_hash,
80+
block_index: block_index
81+
}) do
82+
pk = :aeo_register_tx.account_pubkey(tx)
83+
write_origin(:oracle_register_tx, pk, txi, tx_hash)
84+
Sync.Oracle.register(pk, tx, txi, block_index)
85+
end
86+
87+
def mutate(%__MODULE__{type: :oracle_extend_tx, tx: tx, txi: txi, block_index: block_index}) do
88+
Sync.Oracle.extend(:aeo_extend_tx.oracle_pubkey(tx), tx, txi, block_index)
89+
end
90+
91+
def mutate(%__MODULE__{type: :oracle_response_tx, tx: tx, txi: txi, block_index: block_index}) do
92+
Sync.Oracle.respond(:aeo_response_tx.oracle_pubkey(tx), tx, txi, block_index)
93+
end
94+
95+
def mutate(%__MODULE__{
96+
type: :name_claim_tx,
97+
tx: tx,
98+
txi: txi,
99+
tx_hash: tx_hash,
100+
block_index: block_index
101+
}) do
102+
plain_name = String.downcase(:aens_claim_tx.name(tx))
103+
{:ok, name_hash} = :aens.get_name_hash(plain_name)
104+
write_origin(:name_claim_tx, name_hash, txi, tx_hash)
105+
Sync.Name.claim(plain_name, name_hash, tx, txi, block_index)
106+
end
107+
108+
def mutate(%__MODULE__{type: :name_update_tx, tx: tx, txi: txi, block_index: block_index}) do
109+
Sync.Name.update(:aens_update_tx.name_hash(tx), tx, txi, block_index)
110+
end
111+
112+
def mutate(%__MODULE__{type: :name_transfer_tx, tx: tx, txi: txi, block_index: block_index}) do
113+
Sync.Name.transfer(:aens_transfer_tx.name_hash(tx), tx, txi, block_index)
114+
end
115+
116+
def mutate(%__MODULE__{type: :name_revoke_tx, tx: tx, txi: txi, block_index: block_index}) do
117+
Sync.Name.revoke(:aens_revoke_tx.name_hash(tx), tx, txi, block_index)
118+
end
119+
120+
def mutate(_write_links_mutation) do
121+
:ok
122+
end
123+
124+
defp write_origin(tx_type, pubkey, txi, tx_hash) do
125+
m_origin = Model.origin(index: {tx_type, pubkey, txi}, tx_id: tx_hash)
126+
m_rev_origin = Model.rev_origin(index: {txi, tx_type, pubkey})
127+
:mnesia.write(Model.Origin, m_origin, :write)
128+
:mnesia.write(Model.RevOrigin, m_rev_origin, :write)
129+
write_field(tx_type, nil, pubkey, txi)
130+
end
131+
132+
defp write_field(tx_type, pos, pubkey, txi) do
133+
m_field = Model.field(index: {tx_type, pos, pubkey, txi})
134+
:mnesia.write(Model.Field, m_field, :write)
135+
Model.incr_count({tx_type, pos, pubkey})
136+
end
137+
end
138+
139+
defimpl AeMdw.Db.Mutation, for: AeMdw.Db.WriteLinksMutation do
140+
def mutate(mutation) do
141+
@for.mutate(mutation)
142+
end
143+
end

0 commit comments

Comments
 (0)