Eliminate unused statistics context and gen_server and move statistics function into Chain context.

pull/511/head
Stamates 6 years ago
parent 88174b3617
commit faa53be558
  1. 2
      apps/explorer/config/config.exs
  2. 2
      apps/explorer/config/test.exs
  3. 1
      apps/explorer/lib/explorer/application.ex
  4. 25
      apps/explorer/lib/explorer/chain.ex
  5. 104
      apps/explorer/lib/explorer/chain/statistics.ex
  6. 77
      apps/explorer/lib/explorer/chain/statistics/server.ex
  7. 120
      apps/explorer/test/explorer/chain/statistics/server_test.exs
  8. 68
      apps/explorer/test/explorer/chain/statistics_test.exs
  9. 32
      apps/explorer_web/lib/explorer_web/controllers/chain_controller.ex
  10. 6
      apps/explorer_web/lib/explorer_web/templates/chain/show.html.eex
  11. 8
      apps/explorer_web/test/explorer_web/controllers/chain_controller_test.exs
  12. 4
      apps/explorer_web/test/explorer_web/features/pages/chain_page.ex
  13. 26
      apps/explorer_web/test/explorer_web/features/viewing_chain_test.exs

@ -14,8 +14,6 @@ config :explorer,
config :explorer, Explorer.Integrations.EctoLogger, query_time_ms_threshold: 2_000 config :explorer, Explorer.Integrations.EctoLogger, query_time_ms_threshold: 2_000
config :explorer, Explorer.Chain.Statistics.Server, enabled: true
config :explorer, Explorer.ExchangeRates, enabled: true config :explorer, Explorer.ExchangeRates, enabled: true
config :explorer, Explorer.Market.History.Cataloger, enabled: true config :explorer, Explorer.Market.History.Cataloger, enabled: true

@ -13,8 +13,6 @@ config :explorer, Explorer.Repo,
pool_timeout: 10_000, pool_timeout: 10_000,
ownership_timeout: 60_000 ownership_timeout: 60_000
config :explorer, Explorer.Chain.Statistics.Server, enabled: false
config :explorer, Explorer.ExchangeRates, enabled: false config :explorer, Explorer.ExchangeRates, enabled: false
config :explorer, Explorer.Market.History.Cataloger, enabled: false config :explorer, Explorer.Market.History.Cataloger, enabled: false

@ -24,7 +24,6 @@ defmodule Explorer.Application do
defp configurable_children do defp configurable_children do
[ [
configure(Explorer.Chain.Statistics.Server),
configure(Explorer.ExchangeRates), configure(Explorer.ExchangeRates),
configure(Explorer.Market.History.Cataloger) configure(Explorer.Market.History.Cataloger)
] ]

@ -188,6 +188,31 @@ defmodule Explorer.Chain do
|> Repo.all() |> Repo.all()
end end
@doc """
The average time it took to mine/validate the last <= 100 `t:Explorer.Chain.Block.t/0`
"""
@spec average_block_time() :: non_neg_integer()
def average_block_time() do
{:ok, %Postgrex.Result{rows: [[rows]]}} =
SQL.query(
Repo,
"""
SELECT coalesce(avg(difference), interval '0 seconds')
FROM (
SELECT b.timestamp - lag(b.timestamp) over (order by b.timestamp) as difference
FROM (SELECT * FROM blocks ORDER BY number DESC LIMIT 101) b
LIMIT 100 OFFSET 1
) t
""",
[]
)
{:ok, value} = Timex.Ecto.Time.load(rows)
value
end
@doc """ @doc """
The `t:Explorer.Chain.Address.t/0` `balance` in `unit`. The `t:Explorer.Chain.Address.t/0` `balance` in `unit`.
""" """

@ -1,104 +0,0 @@
defmodule Explorer.Chain.Statistics do
@moduledoc """
Represents statistics about the chain.
"""
import Ecto.Query
alias Ecto.Adapters.SQL
alias Explorer.{Chain, PagingOptions, Repo}
alias Explorer.Chain.{Block, Transaction}
alias Timex.Duration
@average_time_query """
SELECT coalesce(avg(difference), interval '0 seconds')
FROM (
SELECT b.timestamp - lag(b.timestamp) over (order by b.timestamp) as difference
FROM (SELECT * FROM blocks ORDER BY number DESC LIMIT 101) b
LIMIT 100 OFFSET 1
) t
"""
@typedoc """
The number of `t:Explorer.Chain.Block.t/0` mined/validated per minute.
"""
@type blocks_per_minute :: non_neg_integer()
@typedoc """
* `average_time` - the average time it took to mine/validate the last <= 100 `t:Explorer.Chain.Block.t/0`
* `blocks` - the last <= 5 `t:Explorer.Chain.Block.t/0`
(`t:Explorer.Chain.Block.t/0` `timestamp`) and when it was inserted into the databasse
(`t:Explorer.Chain.Block.t/0` `inserted_at`)
* `number` - the latest `t:Explorer.Chain.Block.t/0` `number`
`t:Explorer.Chain.Block.t/0`
* `timestamp` - when the last `t:Explorer.Chain.Block.t/0` was mined/validated
* `transaction_velocity` - the number of `t:Explorer.Chain.Block.t/0` mined/validated in the last minute
* `transactions` - the last <= 5 `t:Explorer.Chain.Transaction.t/0`
"""
@type t :: %__MODULE__{
average_time: Duration.t(),
blocks: [Block.t()],
number: Block.block_number(),
timestamp: :calendar.datetime(),
transactions: [Transaction.t()]
}
defstruct average_time: %Duration{seconds: 0, megaseconds: 0, microseconds: 0},
blocks: [],
number: -1,
timestamp: nil,
transactions: []
def fetch do
blocks =
from(
block in Block,
order_by: [desc: block.number],
preload: [:miner, :transactions],
limit: 4
)
transactions =
Chain.recent_collated_transactions(
necessity_by_association: %{
block: :required,
from_address: :required,
to_address: :optional
},
paging_options: %PagingOptions{page_size: 5}
)
%__MODULE__{
average_time: query_duration(@average_time_query),
blocks: Repo.all(blocks),
transactions: transactions
}
|> put_max_numbered_block()
end
defp put_max_numbered_block(state) do
case Chain.max_numbered_block() do
{:ok, %Block{number: number, timestamp: timestamp}} ->
%__MODULE__{
state
| number: number,
timestamp: timestamp
}
{:error, :not_found} ->
state
end
end
defp query_duration(query) do
results = SQL.query!(Repo, query, [])
{:ok, value} =
results.rows
|> List.first()
|> List.first()
|> Timex.Ecto.Time.load()
value
end
end

@ -1,77 +0,0 @@
defmodule Explorer.Chain.Statistics.Server do
@moduledoc "Stores the latest chain statistics."
use GenServer
require Logger
alias Explorer.Chain.Statistics
@interval 1_000
defstruct statistics: %Statistics{},
task: nil
def child_spec(_) do
Supervisor.Spec.worker(__MODULE__, [[refresh: true]])
end
@spec fetch() :: Statistics.t()
def fetch do
GenServer.call(__MODULE__, :fetch)
end
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@impl GenServer
def init(options) when is_list(options) do
if Keyword.get(options, :refresh, true) do
send(self(), :refresh)
end
{:ok, %__MODULE__{}}
end
@impl GenServer
def handle_info(:refresh, %__MODULE__{task: task} = state) do
new_state =
case task do
nil ->
%__MODULE__{state | task: Task.Supervisor.async_nolink(Explorer.TaskSupervisor, Statistics, :fetch, [])}
_ ->
state
end
{:noreply, new_state}
end
def handle_info({ref, %Statistics{} = statistics}, %__MODULE__{task: %Task{ref: ref}} = state) do
Process.demonitor(ref, [:flush])
Process.send_after(self(), :refresh, @interval)
{:noreply, %__MODULE__{state | statistics: statistics, task: nil}}
end
def handle_info({:DOWN, ref, :process, pid, reason}, %__MODULE__{task: %Task{pid: pid, ref: ref}} = state) do
Logger.error(fn -> "#{inspect(Statistics)}.fetch failed and could not be cached: #{inspect(reason)}" end)
Process.send_after(self(), :refresh, @interval)
{:noreply, %__MODULE__{state | task: nil}}
end
@impl GenServer
def handle_call(:fetch, _, %__MODULE__{statistics: %Statistics{} = statistics} = state),
do: {:reply, statistics, state}
@impl GenServer
def terminate(_, %__MODULE__{task: nil}), do: :ok
def terminate(_, %__MODULE__{task: task}) do
Task.shutdown(task)
end
end

@ -1,120 +0,0 @@
defmodule Explorer.Chain.Statistics.ServerTest do
use Explorer.DataCase, async: false
import ExUnit.CaptureLog
alias Explorer.Chain.Statistics
alias Explorer.Chain.Statistics.Server
# shutdown: "owner exited with: shutdown" error from polluting logs when tests are successful
@moduletag :capture_log
describe "child_spec/1" do
test "it defines a child_spec/1 that works with supervisors" do
insert(:block)
assert {:ok, pid} = start_supervised(Server)
%Server{task: %Task{pid: pid}} = :sys.get_state(pid)
ref = Process.monitor(pid)
assert_receive {:DOWN, ^ref, :process, ^pid, _}
end
end
describe "init/1" do
test "returns a new chain when not told to refresh" do
empty_statistics = %Statistics{}
assert {:ok, %Server{statistics: ^empty_statistics}} = Server.init(refresh: false)
end
test "returns a new Statistics when told to refresh" do
empty_statistics = %Statistics{}
assert {:ok, %Server{statistics: ^empty_statistics}} = Server.init(refresh: true)
end
test "refreshes when told to refresh" do
{:ok, _} = Server.init([])
assert_receive :refresh, 2_000
end
end
describe "handle_info/2" do
setup :state
test "returns the original statistics when sent a :refresh message", %{
state: %Server{statistics: statistics} = state
} do
assert {:noreply, %Server{statistics: ^statistics, task: task}} = Server.handle_info(:refresh, state)
Task.await(task)
end
test "launches a Statistics.fetch Task update when sent a :refresh message", %{state: state} do
assert {:noreply, %Server{task: %Task{} = task}} = Server.handle_info(:refresh, state)
assert %Statistics{} = Task.await(task)
end
test "stores successful Task in state", %{state: state} do
# so that `statistics` from Task will be different
insert(:block)
assert {:noreply, %Server{task: %Task{ref: ref}} = refresh_state} = Server.handle_info(:refresh, state)
assert_receive {^ref, %Statistics{} = message_statistics} = message
assert {:noreply, %Server{statistics: ^message_statistics, task: nil}} =
Server.handle_info(message, refresh_state)
refute message_statistics == state.statistics
end
test "logs crashed Task", %{state: state} do
assert {:noreply, %Server{task: %Task{pid: pid, ref: ref}} = refresh_state} = Server.handle_info(:refresh, state)
Process.exit(pid, :boom)
assert_receive {:DOWN, ^ref, :process, ^pid, :boom} = message
captured_log =
capture_log(fn ->
assert {:noreply, %Server{task: nil}} = Server.handle_info(message, refresh_state)
end)
assert captured_log =~ "Explorer.Chain.Statistics.fetch failed and could not be cached: :boom"
end
end
describe "handle_call/3" do
test "replies with statistics when sent a :fetch message" do
original = Statistics.fetch()
state = %Server{statistics: original}
assert {:reply, ^original, ^state} = Server.handle_call(:fetch, self(), state)
end
end
describe "terminate/2" do
setup :state
test "cleans up in-progress tasks when terminated", %{state: state} do
assert {:noreply, %Server{task: %Task{pid: pid}} = refresh_state} = Server.handle_info(:refresh, state)
second_ref = Process.monitor(pid)
Server.terminate(:boom, refresh_state)
assert_receive {:DOWN, ^second_ref, :process, ^pid, :shutdown}
end
end
defp state(_) do
{:ok, state} = Server.init([])
%{state: state}
end
end

@ -1,68 +0,0 @@
defmodule Explorer.Chain.StatisticsTest do
use Explorer.DataCase
alias Explorer.Chain.Statistics
alias Timex.Duration
describe "fetch/0" do
test "returns -1 for the number when there are no blocks" do
assert %Statistics{number: -1} = Statistics.fetch()
end
test "returns the highest block number when there is a block" do
insert(:block, number: 1)
max_number = 100
insert(:block, number: max_number)
assert %Statistics{number: ^max_number} = Statistics.fetch()
end
test "returns the latest block timestamp" do
time = DateTime.utc_now()
insert(:block, timestamp: time)
statistics = Statistics.fetch()
assert Timex.diff(statistics.timestamp, time, :seconds) == 0
end
test "returns the average time between blocks for the last 100 blocks" do
time = DateTime.utc_now()
insert(:block, timestamp: Timex.shift(time, seconds: -1000))
for x <- 100..0 do
insert(:block, timestamp: Timex.shift(time, seconds: -5 * x))
end
assert %Statistics{
average_time: %Duration{
seconds: 5,
megaseconds: 0,
microseconds: 0
}
} = Statistics.fetch()
end
test "returns the last five blocks" do
insert_list(5, :block)
statistics = Statistics.fetch()
assert statistics.blocks |> Enum.count() == 4
end
test "returns the last five transactions with blocks" do
Enum.map(0..5, fn _ ->
:transaction
|> insert()
|> with_block()
end)
statistics = Statistics.fetch()
assert statistics.transactions |> Enum.count() == 5
end
end
end

@ -1,30 +1,48 @@
defmodule ExplorerWeb.ChainController do defmodule ExplorerWeb.ChainController do
use ExplorerWeb, :controller use ExplorerWeb, :controller
alias Explorer.Chain.{Address, Block, Statistics, Transaction} alias Explorer.{PagingOptions, Repo}
alias Explorer.Chain
alias Explorer.Chain.{Address, Block, Transaction}
alias Explorer.ExchangeRates.Token alias Explorer.ExchangeRates.Token
alias Explorer.Market alias Explorer.Market
alias ExplorerWeb.Chain
def show(conn, _params) do def show(conn, _params) do
transaction_estimated_count = Explorer.Chain.transaction_estimated_count() transaction_estimated_count = Chain.transaction_estimated_count()
address_estimated_count = Explorer.Chain.address_estimated_count() address_estimated_count = Chain.address_estimated_count()
transactions =
Chain.recent_collated_transactions(
necessity_by_association: %{
block: :required,
from_address: :required,
to_address: :optional
},
paging_options: %PagingOptions{page_size: 5}
)
blocks =
[paging_options: %PagingOptions{page_size: 4}]
|> Chain.list_blocks()
|> Repo.preload([:miner, :transactions])
render( render(
conn, conn,
"show.html", "show.html",
address_estimated_count: address_estimated_count, address_estimated_count: address_estimated_count,
chain: Statistics.fetch(), average_block_time: Chain.average_block_time(),
blocks: blocks,
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
market_history_data: Market.fetch_recent_history(30), market_history_data: Market.fetch_recent_history(30),
transaction_estimated_count: transaction_estimated_count transaction_estimated_count: transaction_estimated_count,
transactions: transactions
) )
end end
def search(conn, %{"q" => query}) do def search(conn, %{"q" => query}) do
query query
|> String.trim() |> String.trim()
|> Chain.from_param() |> ExplorerWeb.Chain.from_param()
|> case do |> case do
{:ok, item} -> {:ok, item} ->
redirect_search_results(conn, item) redirect_search_results(conn, item)

@ -28,7 +28,7 @@
<%= gettext "Average block time" %> <%= gettext "Average block time" %>
</span> </span>
<span class="dashboard-banner-network-stats-value" data-selector="average-block-time"> <span class="dashboard-banner-network-stats-value" data-selector="average-block-time">
<%= Timex.format_duration(@chain.average_time, :humanized) %> <%= Timex.format_duration(@average_block_time, :humanized) %>
</span> </span>
</div> </div>
<div class="dashboard-banner-network-stats-item"> <div class="dashboard-banner-network-stats-item">
@ -57,7 +57,7 @@
<%= link(gettext("View All Blocks →"), to: block_path(ExplorerWeb.Endpoint, :index, Gettext.get_locale), class: "button button--secondary button--xsmall float-right") %> <%= link(gettext("View All Blocks →"), to: block_path(ExplorerWeb.Endpoint, :index, Gettext.get_locale), class: "button button--secondary button--xsmall float-right") %>
<h2 class="card-title"><%= gettext "Blocks" %></h2> <h2 class="card-title"><%= gettext "Blocks" %></h2>
<div class="row" data-selector="chain-block-list"> <div class="row" data-selector="chain-block-list">
<%= for block <- @chain.blocks do %> <%= for block <- @blocks do %>
<%= render ExplorerWeb.ChainView, "_block.html", locale: @locale, block: block %> <%= render ExplorerWeb.ChainView, "_block.html", locale: @locale, block: block %>
<% end %> <% end %>
</div> </div>
@ -74,7 +74,7 @@
<%= link(gettext("View All Transactions →"), to: transaction_path(ExplorerWeb.Endpoint, :index, Gettext.get_locale), class: "button button--secondary button--xsmall float-right") %> <%= link(gettext("View All Transactions →"), to: transaction_path(ExplorerWeb.Endpoint, :index, Gettext.get_locale), class: "button button--secondary button--xsmall float-right") %>
<h2 class="card-title"><%= gettext "Transactions" %></h2> <h2 class="card-title"><%= gettext "Transactions" %></h2>
<span data-selector="transactions-list"> <span data-selector="transactions-list">
<%= for transaction <- @chain.transactions do %> <%= for transaction <- @transactions do %>
<%= render ExplorerWeb.TransactionView, "_tile.html", locale: @locale, transaction: transaction %> <%= render ExplorerWeb.TransactionView, "_tile.html", locale: @locale, transaction: transaction %>
<% end %> <% end %>
</span> </span>

@ -22,7 +22,7 @@ defmodule ExplorerWeb.ChainControllerTest do
insert(:block, %{number: 23}) insert(:block, %{number: 23})
conn = get(conn, "/en") conn = get(conn, "/en")
assert(List.first(conn.assigns.chain.blocks).number == 23) assert(List.first(conn.assigns.blocks).number == 23)
end end
test "excludes all but the most recent five blocks", %{conn: conn} do test "excludes all but the most recent five blocks", %{conn: conn} do
@ -30,7 +30,7 @@ defmodule ExplorerWeb.ChainControllerTest do
insert_list(5, :block) insert_list(5, :block)
conn = get(conn, "/en") conn = get(conn, "/en")
refute(Enum.member?(conn.assigns.chain.blocks, old_block)) refute(Enum.member?(conn.assigns.blocks, old_block))
end end
test "only returns transactions with an associated block", %{conn: conn} do test "only returns transactions with an associated block", %{conn: conn} do
@ -43,7 +43,7 @@ defmodule ExplorerWeb.ChainControllerTest do
conn = get(conn, "/en") conn = get(conn, "/en")
transaction_hashes = Enum.map(conn.assigns.chain.transactions, fn transaction -> transaction.hash end) transaction_hashes = Enum.map(conn.assigns.transactions, fn transaction -> transaction.hash end)
assert(Enum.member?(transaction_hashes, associated.hash)) assert(Enum.member?(transaction_hashes, associated.hash))
refute(Enum.member?(transaction_hashes, unassociated.hash)) refute(Enum.member?(transaction_hashes, unassociated.hash))
@ -57,7 +57,7 @@ defmodule ExplorerWeb.ChainControllerTest do
conn = get(conn, "/en") conn = get(conn, "/en")
assert(List.first(conn.assigns.chain.transactions).hash == transaction.hash) assert(List.first(conn.assigns.transactions).hash == transaction.hash)
end end
test "returns market history data", %{conn: conn} do test "returns market history data", %{conn: conn} do

@ -5,7 +5,7 @@ defmodule ExplorerWeb.ChainPage do
import Wallaby.Query, only: [css: 1, css: 2] import Wallaby.Query, only: [css: 1, css: 2]
alias Explorer.Chain.{Block, InternalTransaction, Transaction} alias Explorer.Chain.{Block, Transaction}
def address_count(count) do def address_count(count) do
css("[data-selector='address-count']", text: Integer.to_string(count)) css("[data-selector='address-count']", text: Integer.to_string(count))
@ -23,7 +23,7 @@ defmodule ExplorerWeb.ChainPage do
css("[data-selector='chain-block']", count: count) css("[data-selector='chain-block']", count: count)
end end
def contract_creation(%InternalTransaction{created_contract_address_hash: hash}) do def contract_creation(%Transaction{created_contract_address_hash: hash}) do
css("[data-test='contract-creation'] [data-address-hash='#{hash}']") css("[data-test='contract-creation'] [data-address-hash='#{hash}']")
end end

@ -7,9 +7,9 @@ defmodule ExplorerWeb.ViewingChainTest do
alias ExplorerWeb.{AddressPage, BlockPage, ChainPage, Notifier, TransactionPage} alias ExplorerWeb.{AddressPage, BlockPage, ChainPage, Notifier, TransactionPage}
setup do setup do
[oldest_block | _] = Enum.map(1..4, &insert(:block, number: &1)) [oldest_block | _] = Enum.map(401..404, &insert(:block, number: &1))
block = insert(:block, number: 5) block = insert(:block, number: 405)
[oldest_transaction | _] = [oldest_transaction | _] =
4 4
@ -22,6 +22,7 @@ defmodule ExplorerWeb.ViewingChainTest do
{:ok, {:ok,
%{ %{
block: block,
last_shown_block: oldest_block, last_shown_block: oldest_block,
last_shown_transaction: oldest_transaction last_shown_transaction: oldest_transaction
}} }}
@ -120,6 +121,7 @@ defmodule ExplorerWeb.ViewingChainTest do
test "viewing new transactions via live update", %{ test "viewing new transactions via live update", %{
session: session, session: session,
block: block,
last_shown_transaction: last_shown_transaction last_shown_transaction: last_shown_transaction
} do } do
session session
@ -129,7 +131,7 @@ defmodule ExplorerWeb.ViewingChainTest do
transaction = transaction =
:transaction :transaction
|> insert() |> insert()
|> with_block() |> with_block(block)
Notifier.handle_event({:chain_event, :transactions, [transaction.hash]}) Notifier.handle_event({:chain_event, :transactions, [transaction.hash]})
@ -139,11 +141,11 @@ defmodule ExplorerWeb.ViewingChainTest do
|> refute_has(ChainPage.transaction(last_shown_transaction)) |> refute_has(ChainPage.transaction(last_shown_transaction))
end end
test "count of non-loaded transactions live update when batch overflow", %{session: session} do test "count of non-loaded transactions live update when batch overflow", %{session: session, block: block} do
transaction_hashes = transaction_hashes =
30 30
|> insert_list(:transaction) |> insert_list(:transaction)
|> with_block() |> with_block(block)
|> Enum.map(& &1.hash) |> Enum.map(& &1.hash)
session session
@ -155,23 +157,23 @@ defmodule ExplorerWeb.ViewingChainTest do
assert_has(session, ChainPage.non_loaded_transaction_count("30")) assert_has(session, ChainPage.non_loaded_transaction_count("30"))
end end
test "contract creation is shown for to_address", %{session: session} do test "contract creation is shown for to_address", %{session: session, block: block} do
contract_address = insert(:contract_address) contract_address = insert(:contract_address)
transaction = transaction =
:transaction :transaction
|> insert(to_address: nil) |> insert(to_address: nil)
|> with_contract_creation(contract_address) |> with_contract_creation(contract_address)
|> with_block() |> with_block(block)
internal_transaction = # internal_transaction =
:internal_transaction_create # :internal_transaction_create
|> insert(transaction: transaction, index: 0) # |> insert(transaction: transaction, index: 0)
|> with_contract_creation(contract_address) # |> with_contract_creation(contract_address)
session session
|> ChainPage.visit_page() |> ChainPage.visit_page()
|> assert_has(ChainPage.contract_creation(internal_transaction)) |> assert_has(ChainPage.contract_creation(transaction))
end end
end end
end end

Loading…
Cancel
Save