Merge pull request #1170 from poanetwork/ams-address-count

Add a cache to count the addresses with balance > 0.
pull/1168/head
Amanda 6 years ago committed by GitHub
commit 9d620f2530
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      apps/block_scout_web/lib/block_scout_web/controllers/address_controller.ex
  2. 2
      apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex
  3. 2
      apps/block_scout_web/lib/block_scout_web/notifier.ex
  4. 6
      apps/block_scout_web/lib/block_scout_web/templates/address/index.html.eex
  5. 2
      apps/block_scout_web/lib/block_scout_web/templates/chain/show.html.eex
  6. 6
      apps/explorer/config/config.exs
  7. 8
      apps/explorer/config/test.exs
  8. 3
      apps/explorer/lib/explorer/application.ex
  9. 34
      apps/explorer/lib/explorer/chain.ex
  10. 11
      apps/explorer/lib/explorer/chain/address.ex
  11. 117
      apps/explorer/lib/explorer/counters/addresses_with_balance_counter.ex
  12. 33
      apps/explorer/lib/explorer/counters/block_validation_counter.ex
  13. 11
      apps/explorer/test/explorer/chain/address_test.exs
  14. 17
      apps/explorer/test/explorer/chain_test.exs
  15. 15
      apps/explorer/test/explorer/counters/addresses_with_balance_counter_test.exs

@ -8,7 +8,7 @@ defmodule BlockScoutWeb.AddressController do
def index(conn, _params) do def index(conn, _params) do
render(conn, "index.html", render(conn, "index.html",
address_tx_count_pairs: Chain.list_top_addresses(), address_tx_count_pairs: Chain.list_top_addresses(),
address_estimated_count: Chain.address_estimated_count(), address_count: Chain.count_addresses_with_balance_from_cache(),
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
total_supply: Chain.total_supply() total_supply: Chain.total_supply()
) )

@ -36,7 +36,7 @@ defmodule BlockScoutWeb.ChainController do
render( render(
conn, conn,
"show.html", "show.html",
address_estimated_count: Chain.address_estimated_count(), address_count: Chain.count_addresses_with_balance_from_cache(),
average_block_time: Chain.average_block_time(), average_block_time: Chain.average_block_time(),
blocks: blocks, blocks: blocks,
exchange_rate: exchange_rate, exchange_rate: exchange_rate,

@ -10,7 +10,7 @@ defmodule BlockScoutWeb.Notifier do
alias Explorer.ExchangeRates.Token alias Explorer.ExchangeRates.Token
def handle_event({:chain_event, :addresses, :realtime, addresses}) do def handle_event({:chain_event, :addresses, :realtime, addresses}) do
Endpoint.broadcast("addresses:new_address", "count", %{count: Chain.address_estimated_count()}) Endpoint.broadcast("addresses:new_address", "count", %{count: Chain.count_addresses_with_balance_from_cache()})
addresses addresses
|> Stream.reject(fn %Address{fetched_coin_balance: fetched_coin_balance} -> is_nil(fetched_coin_balance) end) |> Stream.reject(fn %Address{fetched_coin_balance: fetched_coin_balance} -> is_nil(fetched_coin_balance) end)

@ -4,16 +4,16 @@
<h1><%= gettext "Addresses" %></h1> <h1><%= gettext "Addresses" %></h1>
<p> <p>
<%= gettext "Showing 250 addresses of" %> <%= gettext "Showing 250 addresses of" %>
<%= Cldr.Number.to_string!(@address_estimated_count, format: "#,###") %> <%= Cldr.Number.to_string!(@address_count, format: "#,###") %>
<%= gettext "total addresses with a balance" %> <%= gettext "total addresses with a balance" %>
</p> </p>
<span data-selector="top-addresses-list"> <span data-selector="top-addresses-list">
<%= for {{address, tx_count}, index} <- Enum.with_index(@address_tx_count_pairs, 1) do %> <%= for {{address, tx_count}, index} <- Enum.with_index(@address_tx_count_pairs, 1) do %>
<%= render "_tile.html", <%= render "_tile.html",
address: address, index: index, exchange_rate: @exchange_rate, address: address, index: index, exchange_rate: @exchange_rate,
total_supply: @total_supply, tx_count: tx_count, total_supply: @total_supply, tx_count: tx_count,
validation_count: validation_count(address) %> validation_count: validation_count(address) %>
<% end %> <% end %>
</span> </span>
</div> </div>

@ -43,7 +43,7 @@
<%= gettext "Wallet addresses" %> <%= gettext "Wallet addresses" %>
</span> </span>
<span class="dashboard-banner-network-stats-value" data-selector="address-count"> <span class="dashboard-banner-network-stats-value" data-selector="address-count">
<%= Cldr.Number.to_string!(@address_estimated_count, format: "#,###") %> <%= Cldr.Number.to_string!(@address_count, format: "#,###") %>
</span> </span>
</div> </div>
</div> </div>

@ -17,8 +17,6 @@ config :explorer, Explorer.Integrations.EctoLogger, query_time_ms_threshold: 2_0
config :explorer, Explorer.ExchangeRates, enabled: true config :explorer, Explorer.ExchangeRates, enabled: true
config :explorer, Explorer.Counters.BlockValidationCounter, enabled: true
config :explorer, Explorer.Market.History.Cataloger, enabled: true config :explorer, Explorer.Market.History.Cataloger, enabled: true
config :explorer, Explorer.Repo, config :explorer, Explorer.Repo,
@ -32,8 +30,12 @@ config :explorer, Explorer.Tracer,
config :explorer, Explorer.Counters.TokenTransferCounter, enabled: true config :explorer, Explorer.Counters.TokenTransferCounter, enabled: true
config :explorer, Explorer.Counters.BlockValidationCounter, enabled: true, enable_consolidation: true
config :explorer, Explorer.Counters.TokenHoldersCounter, enabled: true, enable_consolidation: true config :explorer, Explorer.Counters.TokenHoldersCounter, enabled: true, enable_consolidation: true
config :explorer, Explorer.Counters.AddessesWithBalanceCounter, enabled: true, enable_consolidation: true
if System.get_env("SUPPLY_MODULE") == "TransactionAndLog" do if System.get_env("SUPPLY_MODULE") == "TransactionAndLog" do
config :explorer, supply: Explorer.Chain.Supply.TransactionAndLog config :explorer, supply: Explorer.Chain.Supply.TransactionAndLog
end end

@ -3,8 +3,6 @@ use Mix.Config
# Lower hashing rounds for faster tests # Lower hashing rounds for faster tests
config :bcrypt_elixir, log_rounds: 4 config :bcrypt_elixir, log_rounds: 4
config :explorer, Explorer.Counters.TokenHoldersCounter, enabled: true, enable_consolidation: false
# Configure your database # Configure your database
config :explorer, Explorer.Repo, config :explorer, Explorer.Repo,
adapter: Ecto.Adapters.Postgres, adapter: Ecto.Adapters.Postgres,
@ -21,6 +19,12 @@ config :explorer, Explorer.Market.History.Cataloger, enabled: false
config :explorer, Explorer.Tracer, disabled?: false config :explorer, Explorer.Tracer, disabled?: false
config :explorer, Explorer.Counters.BlockValidationCounter, enabled: true, enable_consolidation: false
config :explorer, Explorer.Counters.TokenHoldersCounter, enabled: true, enable_consolidation: false
config :explorer, Explorer.Counters.AddessesWithBalanceCounter, enabled: true, enable_consolidation: false
config :logger, :explorer, config :logger, :explorer,
level: :warn, level: :warn,
path: Path.absname("logs/test/explorer.log") path: Path.absname("logs/test/explorer.log")

@ -35,7 +35,8 @@ defmodule Explorer.Application do
configure(Explorer.Market.History.Cataloger), configure(Explorer.Market.History.Cataloger),
configure(Explorer.Counters.TokenHoldersCounter), configure(Explorer.Counters.TokenHoldersCounter),
configure(Explorer.Counters.TokenTransferCounter), configure(Explorer.Counters.TokenTransferCounter),
configure(Explorer.Counters.BlockValidationCounter) configure(Explorer.Counters.BlockValidationCounter),
configure(Explorer.Counters.AddessesWithBalanceCounter)
] ]
|> List.flatten() |> List.flatten()
end end

@ -40,7 +40,13 @@ defmodule Explorer.Chain do
alias Explorer.Chain.Block.Reward alias Explorer.Chain.Block.Reward
alias Explorer.{PagingOptions, Repo} alias Explorer.{PagingOptions, Repo}
alias Explorer.Counters.{BlockValidationCounter, TokenHoldersCounter, TokenTransferCounter}
alias Explorer.Counters.{
AddessesWithBalanceCounter,
BlockValidationCounter,
TokenHoldersCounter,
TokenTransferCounter
}
alias Dataloader.Ecto, as: DataloaderEcto alias Dataloader.Ecto, as: DataloaderEcto
@ -82,18 +88,24 @@ defmodule Explorer.Chain do
@typep paging_options :: {:paging_options, PagingOptions.t()} @typep paging_options :: {:paging_options, PagingOptions.t()}
@doc """ @doc """
Gets an estimated count of `t:Explorer.Chain.Address.t/0`'s where the `fetched_coin_balance` is > 0 Gets from the cache the count of `t:Explorer.Chain.Address.t/0`'s where the `fetched_coin_balance` is > 0
""" """
@spec address_estimated_count :: non_neg_integer() @spec count_addresses_with_balance_from_cache :: non_neg_integer()
def address_estimated_count do def count_addresses_with_balance_from_cache do
{:ok, %Postgrex.Result{rows: result}} = AddessesWithBalanceCounter.fetch()
Repo.query(""" end
EXPLAIN SELECT COUNT(a0.hash) FROM addresses AS a0 WHERE (a0.fetched_coin_balance > 0)
""") @doc """
Counts the number of addresses with fetched coin balance > 0.
{[explain], _} = List.pop_at(result, 1) This function should be used with caution. In larger databases, it may take a
[[_ | [rows]]] = Regex.scan(~r/rows=(\d+)/, explain) while to have the return back.
String.to_integer(rows) """
def count_addresses_with_balance do
Repo.one(
Address.count_with_fetched_coin_balance(),
timeout: :infinity
)
end end
@doc """ @doc """

@ -104,4 +104,15 @@ defmodule Explorer.Chain.Address do
@protocol.to_string(hash) @protocol.to_string(hash)
end end
end end
@doc """
Counts all the addresses where the `fetched_coin_balance` is > 0.
"""
def count_with_fetched_coin_balance do
from(
a in Address,
select: fragment("COUNT(*)"),
where: a.fetched_coin_balance > ^0
)
end
end end

@ -0,0 +1,117 @@
defmodule Explorer.Counters.AddessesWithBalanceCounter do
@moduledoc """
Caches the number of addresses with fetched coin balance > 0.
It loads the count asynchronously and in a time interval of 30 minutes.
"""
use GenServer
alias Explorer.Chain
@table :addresses_with_balance_counter
@cache_key "addresses_with_balance"
def table_name do
@table
end
def cache_key do
@cache_key
end
# It is undesirable to automatically start the consolidation in all environments.
# Consider the test environment: if the consolidation initiates but does not
# finish before a test ends, that test will fail. This way, hundreds of
# tests were failing before disabling the consolidation and the scheduler in
# the test env.
config = Application.get_env(:explorer, Explorer.Counters.AddessesWithBalanceCounter)
@enable_consolidation Keyword.get(config, :enable_consolidation)
@doc """
Starts a process to periodically update the counter of the token holders.
"""
@spec start_link(term()) :: GenServer.on_start()
def start_link(_) do
GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
end
@impl true
def init(args) do
create_table()
if enable_consolidation?() do
Task.start_link(&consolidate/0)
schedule_next_consolidation()
end
{:ok, args}
end
def create_table do
opts = [
:set,
:named_table,
:public,
read_concurrency: true
]
:ets.new(table_name(), opts)
end
defp schedule_next_consolidation do
if enable_consolidation?() do
Process.send_after(self(), :consolidate, :timer.minutes(30))
end
end
@doc """
Inserts new items into the `:ets` table.
"""
def insert_counter({key, info}) do
:ets.insert(table_name(), {key, info})
end
@impl true
def handle_info(:consolidate, state) do
consolidate()
schedule_next_consolidation()
{:noreply, state}
end
@doc """
Fetches the info for a specific item from the `:ets` table.
"""
def fetch do
do_fetch(:ets.lookup(table_name(), cache_key()))
end
defp do_fetch([{_, result}]), do: result
defp do_fetch([]), do: 0
@doc """
Consolidates the info by populating the `:ets` table with the current database information.
"""
def consolidate do
counter = Chain.count_addresses_with_balance()
insert_counter({cache_key(), counter})
end
@doc """
Returns a boolean that indicates whether consolidation is enabled
In order to choose whether or not to enable the scheduler and the initial
consolidation, change the following Explorer config:
`config :explorer, Explorer.Counters.AddressesWithBalanceCounter, enable_consolidation: true`
to:
`config :explorer, Explorer.Counters.AddressesWithBalanceCounter, enable_consolidation: false`
"""
def enable_consolidation?, do: @enable_consolidation
end

@ -2,7 +2,7 @@ defmodule Explorer.Counters.BlockValidationCounter do
use GenServer use GenServer
@moduledoc """ @moduledoc """
Module responsible for fetching and consolidating the number of Module responsible for fetching and consolidating the number of
validations from an address. validations from an address.
""" """
@ -15,6 +15,14 @@ defmodule Explorer.Counters.BlockValidationCounter do
@table @table
end end
# It is undesirable to automatically start the consolidation in all environments.
# Consider the test environment: if the consolidation initiates but does not
# finish before a test ends, that test will fail. This way, hundreds of
# tests were failing before disabling the consolidation and the scheduler in
# the test env.
config = Application.get_env(:explorer, Explorer.Counters.BlockValidationCounter)
@enable_consolidation Keyword.get(config, :enable_consolidation)
@doc """ @doc """
Creates a process to continually monitor the validation counts. Creates a process to continually monitor the validation counts.
""" """
@ -28,7 +36,9 @@ defmodule Explorer.Counters.BlockValidationCounter do
def init(args) do def init(args) do
create_table() create_table()
Task.start_link(&consolidate_blocks/0) if enable_consolidation?() do
Task.start_link(&consolidate_blocks/0)
end
Chain.subscribe_to_events(:blocks) Chain.subscribe_to_events(:blocks)
@ -40,8 +50,7 @@ defmodule Explorer.Counters.BlockValidationCounter do
:set, :set,
:named_table, :named_table,
:public, :public,
read_concurrency: true, read_concurrency: true
write_concurrency: true
] ]
:ets.new(table_name(), opts) :ets.new(table_name(), opts)
@ -59,7 +68,7 @@ defmodule Explorer.Counters.BlockValidationCounter do
end end
@doc """ @doc """
Fetches the number of validations related to an `address_hash`. Fetches the number of validations related to an `address_hash`.
""" """
@spec fetch(Hash.Address.t()) :: non_neg_integer @spec fetch(Hash.Address.t()) :: non_neg_integer
def fetch(addr_hash) do def fetch(addr_hash) do
@ -91,4 +100,18 @@ defmodule Explorer.Counters.BlockValidationCounter do
:ets.update_counter(table_name(), string_addr, number, default) :ets.update_counter(table_name(), string_addr, number, default)
end end
@doc """
Returns a boolean that indicates whether consolidation is enabled
In order to choose whether or not to enable the scheduler and the initial
consolidation, change the following Explorer config:
`config :explorer, Explorer.Counters.BlockValidationCounter, enable_consolidation: true`
to:
`config :explorer, Explorer.Counters.BlockValidationCounter, enable_consolidation: false`
"""
def enable_consolidation?, do: @enable_consolidation
end end

@ -2,6 +2,7 @@ defmodule Explorer.Chain.AddressTest do
use Explorer.DataCase use Explorer.DataCase
alias Explorer.Chain.Address alias Explorer.Chain.Address
alias Explorer.Repo
describe "changeset/2" do describe "changeset/2" do
test "with valid attributes" do test "with valid attributes" do
@ -15,4 +16,14 @@ defmodule Explorer.Chain.AddressTest do
refute changeset.valid? refute changeset.valid?
end end
end end
describe "count_with_fetched_coin_balance/0" do
test "returns the number of addresses with fetched_coin_balance greater than 0" do
insert(:address, fetched_coin_balance: 0)
insert(:address, fetched_coin_balance: 1)
insert(:address, fetched_coin_balance: 2)
assert Repo.one(Address.count_with_fetched_coin_balance()) == 2
end
end
end end

@ -24,13 +24,22 @@ defmodule Explorer.ChainTest do
alias Explorer.Chain.Supply.ProofOfAuthority alias Explorer.Chain.Supply.ProofOfAuthority
alias Explorer.Counters.TokenHoldersCounter alias Explorer.Counters.{AddessesWithBalanceCounter, TokenHoldersCounter}
doctest Explorer.Chain doctest Explorer.Chain
describe "address_estimated_count/1" do describe "count_addresses_with_balance_from_cache/0" do
test "returns integer" do test "returns the number of addresses with fetched_coin_balance > 0" do
assert is_integer(Chain.address_estimated_count()) insert(:address, fetched_coin_balance: 0)
insert(:address, fetched_coin_balance: 1)
insert(:address, fetched_coin_balance: 2)
AddessesWithBalanceCounter.consolidate()
addresses_with_balance = Chain.count_addresses_with_balance_from_cache()
assert is_integer(addresses_with_balance)
assert addresses_with_balance == 2
end end
end end

@ -0,0 +1,15 @@
defmodule Explorer.Counters.AddessesWithBalanceCounterTest do
use Explorer.DataCase
alias Explorer.Counters.AddessesWithBalanceCounter
test "populates the cache with the number of addresses with fetched coin balance greater than 0" do
insert(:address, fetched_coin_balance: 0)
insert(:address, fetched_coin_balance: 1)
insert(:address, fetched_coin_balance: 2)
AddessesWithBalanceCounter.consolidate()
assert AddessesWithBalanceCounter.fetch() == 2
end
end
Loading…
Cancel
Save