Skip to content

Commit 045ee35

Browse files
authored
feat: add cursor-based pagination to stats (#384)
1 parent cee643e commit 045ee35

File tree

3 files changed

+159
-58
lines changed

3 files changed

+159
-58
lines changed

lib/ae_mdw/stats.ex

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
defmodule AeMdw.Stats do
2+
@moduledoc """
3+
Context module for dealing with Stats.
4+
"""
5+
6+
alias AeMdw.Blocks
7+
alias AeMdw.Collection
8+
alias AeMdw.Db.Format
9+
alias AeMdw.Db.Model
10+
alias AeMdw.Mnesia
11+
12+
require Model
13+
14+
@type stat() :: map()
15+
@type sum_stat() :: map()
16+
@type cursor() :: binary() | nil
17+
18+
@typep height() :: Blocks.height()
19+
@typep direction() :: Mnesia.direction()
20+
@typep limit() :: Mnesia.limit()
21+
@typep range() :: {:gen, Range.t()} | nil
22+
23+
@table Model.Stat
24+
@sum_table Model.SumStat
25+
26+
@spec fetch_stats(direction(), range(), cursor(), limit()) :: {[stat()], cursor()}
27+
def fetch_stats(direction, range, cursor, limit) do
28+
{:ok, {last_gen, -1}} = Mnesia.last_key(AeMdw.Db.Model.Block)
29+
30+
range_scope = deserialize_scope(range)
31+
32+
cursor_scope =
33+
case deserialize_cursor(cursor) do
34+
nil -> nil
35+
cursor when direction == :forward -> {cursor, cursor + limit + 1}
36+
cursor -> {cursor, cursor - limit - 1}
37+
end
38+
39+
global_scope = if direction == :forward, do: {1, last_gen}, else: {last_gen, 1}
40+
41+
case intersect_scopes([range_scope, cursor_scope, global_scope], direction) do
42+
{:ok, first, last} ->
43+
{gens, next_cursor} = Collection.paginate(first..last, limit)
44+
45+
{render_stats(gens), serialize_cursor(next_cursor)}
46+
47+
:error ->
48+
{[], nil}
49+
end
50+
end
51+
52+
@spec fetch_sum_stats(direction(), range(), cursor(), limit()) :: {[sum_stat()], cursor()}
53+
def fetch_sum_stats(direction, range, cursor, limit) do
54+
{:ok, {last_gen, -1}} = Mnesia.last_key(AeMdw.Db.Model.Block)
55+
56+
range_scope = deserialize_scope(range)
57+
58+
cursor_scope =
59+
case deserialize_cursor(cursor) do
60+
nil -> nil
61+
cursor when direction == :forward -> {cursor, cursor + limit + 1}
62+
cursor -> {cursor, cursor - limit - 1}
63+
end
64+
65+
global_scope = if direction == :forward, do: {0, last_gen}, else: {last_gen, 0}
66+
67+
case intersect_scopes([range_scope, cursor_scope, global_scope], direction) do
68+
{:ok, first, last} ->
69+
{gens, next_cursor} = Collection.paginate(first..last, limit)
70+
71+
{render_sum_stats(gens), serialize_cursor(next_cursor)}
72+
73+
:error ->
74+
{[], nil}
75+
end
76+
end
77+
78+
@spec fetch_stat!(height()) :: stat()
79+
def fetch_stat!(height), do: render_stat(Mnesia.fetch!(@table, height))
80+
81+
@spec fetch_sum_stat!(height()) :: sum_stat()
82+
def fetch_sum_stat!(height), do: render_sum_stat(Mnesia.fetch!(@sum_table, height))
83+
84+
defp intersect_scopes(scopes, direction) do
85+
scopes
86+
|> Enum.reject(&is_nil/1)
87+
|> Enum.reduce(fn
88+
{first, last}, {acc_first, acc_last} when direction == :forward ->
89+
{max(first, acc_first), min(last, acc_last)}
90+
91+
{first, last}, {acc_first, acc_last} ->
92+
{min(first, acc_first), max(last, acc_last)}
93+
end)
94+
|> case do
95+
{first, last} when direction == :forward and first <= last -> {:ok, first, last}
96+
{_first, _last} when direction == :forward -> :error
97+
{first, last} when direction == :backward and first >= last -> {:ok, first, last}
98+
{_first, _last} when direction == :backward -> :error
99+
end
100+
end
101+
102+
defp render_stats(gens), do: Enum.map(gens, &fetch_stat!/1)
103+
104+
defp render_sum_stats(gens), do: Enum.map(gens, &fetch_sum_stat!/1)
105+
106+
defp render_stat(stat), do: Format.to_map(stat, @table)
107+
108+
defp render_sum_stat(sum_stat), do: Format.to_map(sum_stat, @sum_table)
109+
110+
defp serialize_cursor(nil), do: nil
111+
112+
defp serialize_cursor(gen), do: Integer.to_string(gen)
113+
114+
defp deserialize_cursor(nil), do: nil
115+
116+
defp deserialize_cursor(cursor_bin) do
117+
case Integer.parse(cursor_bin) do
118+
{n, ""} when n >= 0 -> n
119+
{_n, _rest} -> nil
120+
:error -> nil
121+
end
122+
end
123+
124+
defp deserialize_scope(nil), do: nil
125+
126+
defp deserialize_scope({:gen, %Range{first: first_gen, last: last_gen}}),
127+
do: {first_gen, last_gen}
128+
end
Lines changed: 30 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,45 @@
11
defmodule AeMdwWeb.StatsController do
22
use AeMdwWeb, :controller
33

4-
alias AeMdw.Db.Model
4+
alias AeMdw.Stats
5+
alias AeMdwWeb.Plugs.PaginatedPlug
6+
alias Plug.Conn
57

6-
alias AeMdw.Db
7-
alias AeMdw.Db.Stream, as: DBS
8-
alias AeMdwWeb.Continuation, as: Cont
9-
alias DBS.Resource.Util, as: RU
8+
plug(PaginatedPlug)
109

11-
require Model
10+
@spec stats(Conn.t(), map()) :: Conn.t()
11+
def stats(%Conn{assigns: assigns, request_path: path} = conn, _params) do
12+
%{direction: direction, limit: limit, cursor: cursor, scope: scope} = assigns
1213

13-
import AeMdwWeb.Util
14-
import AeMdw.Db.Util
14+
{stats, next_cursor} = Stats.fetch_stats(direction, scope, cursor, limit)
1515

16-
##########
16+
uri =
17+
if next_cursor do
18+
%URI{
19+
path: path,
20+
query: URI.encode_query(%{"cursor" => next_cursor, "limit" => limit})
21+
}
22+
|> URI.to_string()
23+
end
1724

18-
def stream_plug_hook(%Plug.Conn{path_info: [_ | rem]} = conn) do
19-
alias AeMdwWeb.DataStreamPlug, as: P
20-
21-
P.handle_assign(
22-
conn,
23-
P.parse_scope((rem == [] && ["backward"]) || rem, ["gen"]),
24-
P.parse_offset(conn.params),
25-
{:ok, %{}}
26-
)
25+
json(conn, %{"data" => stats, "next" => uri})
2726
end
2827

29-
##########
30-
31-
def stats(conn, _params),
32-
do: handle_input(conn, fn -> Cont.response(conn, &json/2) end)
33-
34-
def sum_stats(conn, _params),
35-
do: handle_input(conn, fn -> Cont.response(conn, &json/2) end)
28+
@spec sum_stats(Conn.t(), map()) :: Conn.t()
29+
def sum_stats(%Conn{assigns: assigns, request_path: path} = conn, _params) do
30+
%{direction: direction, limit: limit, cursor: cursor, scope: scope} = assigns
3631

37-
##########
32+
{stats, next_cursor} = Stats.fetch_sum_stats(direction, scope, cursor, limit)
3833

39-
def db_stream(:stats, _params, scope) do
40-
{{start, _}, _dir, succ} = progress(scope)
41-
# TODO: review use of scope_checker
42-
# scope_checker = scope_checker(dir, range)
43-
advance = RU.advance_signal_fn(succ, fn _ -> true end)
34+
uri =
35+
if next_cursor do
36+
%URI{
37+
path: path,
38+
query: URI.encode_query(%{"cursor" => next_cursor, "limit" => limit})
39+
}
40+
|> URI.to_string()
41+
end
4442

45-
RU.signalled_resource({{true, start}, advance}, Model.Stat, &Db.Format.to_map(&1, Model.Stat))
43+
json(conn, %{"data" => stats, "next" => uri})
4644
end
47-
48-
def db_stream(:sum_stats, _params, scope) do
49-
{{start, _}, _dir, succ} = progress(scope)
50-
# TODO: review use of scope_checker
51-
# scope_checker = scope_checker(dir, range)
52-
advance = RU.advance_signal_fn(succ, fn _ -> true end)
53-
54-
RU.signalled_resource(
55-
{{true, start}, advance},
56-
Model.SumStat,
57-
&Db.Format.to_map(&1, Model.SumStat)
58-
)
59-
end
60-
61-
def progress({:gen, %Range{first: f, last: l}}) do
62-
cond do
63-
f <= l -> {{f, l}, :forward, &next/2}
64-
f > l -> {{f, l}, :backward, &prev/2}
65-
end
66-
end
67-
68-
def scope_checker(:forward, {f, l}), do: fn x -> x >= f && x <= l end
69-
def scope_checker(:backward, {f, l}), do: fn x -> x >= l && x <= f end
7045
end

lib/ae_mdw_web/router.ex

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@ defmodule AeMdwWeb.Router do
66
@paginables [
77
{["names", "auctions"], &AeMdwWeb.NameController.stream_plug_hook/1},
88
{["contracts", "logs"], &AeMdwWeb.ContractController.stream_plug_hook/1},
9-
{["contracts", "calls"], &AeMdwWeb.ContractController.stream_plug_hook/1},
10-
{["stats"], &AeMdwWeb.StatsController.stream_plug_hook/1},
11-
{["totalstats"], &AeMdwWeb.StatsController.stream_plug_hook/1}
9+
{["contracts", "calls"], &AeMdwWeb.ContractController.stream_plug_hook/1}
1210
]
1311

1412
@scopes ["gen", "txi"]

0 commit comments

Comments
 (0)