diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex index 556cff6bba..6849f0f94c 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex @@ -3,7 +3,7 @@ defmodule BlockScoutWeb.ChainController do alias BlockScoutWeb.ChainView alias Explorer.{Chain, PagingOptions, Repo} - alias Explorer.Chain.{Address, Block, Transaction} + alias Explorer.Chain.{Address, Block, BlockCountCache, Transaction} alias Explorer.Counters.AverageBlockTime alias Explorer.ExchangeRates.Token alias Explorer.Market @@ -11,7 +11,7 @@ defmodule BlockScoutWeb.ChainController do def show(conn, _params) do transaction_estimated_count = Chain.transaction_estimated_count() - block_count = Chain.block_consensus_count() + block_count = BlockCountCache.count() exchange_rate = Market.get_exchange_rate(Explorer.coin()) || Token.null() diff --git a/apps/explorer/config/config.exs b/apps/explorer/config/config.exs index cc5898c351..fdbde4d497 100644 --- a/apps/explorer/config/config.exs +++ b/apps/explorer/config/config.exs @@ -78,6 +78,9 @@ config :spandex_ecto, SpandexEcto.EctoLogger, tracer: Explorer.Tracer, otp_app: :explorer +config :explorer, Explorer.Chain.BlockCountCache, + ttl: System.get_env("BLOCK_COUNT_CACHE_TTL") + # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. import_config "#{Mix.env()}.exs" diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index 65f91c1d4b..080b880211 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -6,7 +6,7 @@ defmodule Explorer.Application do use Application alias Explorer.Admin - alias Explorer.Chain.{BlockNumberCache, TransactionCountCache} + alias Explorer.Chain.{BlockCountCache, BlockNumberCache, TransactionCountCache} alias Explorer.Repo.PrometheusLogger @impl Application @@ -38,6 +38,7 @@ defmodule Explorer.Application do res = Supervisor.start_link(children, opts) BlockNumberCache.setup() + BlockCountCache.setup() res end diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 42ed40a0bd..beee541ebf 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -1042,7 +1042,7 @@ defmodule Explorer.Chain do end end - @spec fetch_min_and_max_block_numbers() :: {non_neg_integer(), non_neg_integer} + @spec fetch_min_and_max_block_numbers() :: {non_neg_integer, non_neg_integer} def fetch_min_and_max_block_numbers do query = from(block in Block, @@ -1058,6 +1058,17 @@ defmodule Explorer.Chain do end end + @spec fetch_count_consensus_block() :: non_neg_integer + def fetch_count_consensus_block do + query = + from(block in Block, + select: count(block.hash), + where: block.consensus == true + ) + + Repo.one!(query) + end + @doc """ The number of `t:Explorer.Chain.InternalTransaction.t/0`. diff --git a/apps/explorer/lib/explorer/chain/block_count_cache.ex b/apps/explorer/lib/explorer/chain/block_count_cache.ex new file mode 100644 index 0000000000..7c900bfedd --- /dev/null +++ b/apps/explorer/lib/explorer/chain/block_count_cache.ex @@ -0,0 +1,84 @@ +defmodule Explorer.Chain.BlockCountCache do + @moduledoc """ + Cache for count consensus blocks. + """ + + alias Explorer.Chain + + @tab :block_count_cache + # 1 minutes + @cache_period 1_000 * 60 + @key "count" + @opts_key "opts" + + @spec setup() :: :ok + def setup do + if :ets.whereis(@tab) == :undefined do + :ets.new(@tab, [ + :set, + :named_table, + :public, + write_concurrency: true + ]) + end + + setup_opts() + update_cache() + + :ok + end + + def count do + initial_cache = {_count, old_current_time} = cached_values() + + {count, _current_time} = + if current_time() - old_current_time > cache_period() do + update_cache() + + cached_values() + else + initial_cache + end + + count + end + + defp update_cache do + current_time = current_time() + count = count_from_db() + tuple = {count, current_time} + + :ets.insert(@tab, {@key, tuple}) + end + + defp setup_opts do + cache_period = Application.get_env(:explorer, __MODULE__)[:ttl] || @cache_period + + :ets.insert(@tab, {@opts_key, cache_period}) + end + + defp cached_values do + [{_, cached_values}] = :ets.lookup(@tab, @key) + + cached_values + end + + defp cache_period do + [{_, cache_period}] = :ets.lookup(@tab, @opts_key) + + cache_period + end + + defp count_from_db do + Chain.fetch_count_consensus_block() + rescue + _e -> + 0 + end + + defp current_time do + utc_now = DateTime.utc_now() + + DateTime.to_unix(utc_now, :millisecond) + end +end diff --git a/apps/explorer/test/explorer/chain/block_count_cache_test.exs b/apps/explorer/test/explorer/chain/block_count_cache_test.exs new file mode 100644 index 0000000000..9b3795d15e --- /dev/null +++ b/apps/explorer/test/explorer/chain/block_count_cache_test.exs @@ -0,0 +1,45 @@ +defmodule Explorer.Chain.BlockCountCacheTest do + use Explorer.DataCase + + alias Explorer.Chain.BlockCountCache + + describe "count/0" do + test "return count" do + insert(:block, number: 1, consensus: true) + insert(:block, number: 2, consensus: true) + insert(:block, number: 3, consensus: false) + + BlockCountCache.setup() + + assert BlockCountCache.count() == 2 + end + + test "invalidates cache if period did pass" do + insert(:block, number: 1, consensus: true) + + Application.put_env(:explorer, BlockCountCache, ttl: 2_00) + BlockCountCache.setup() + + assert BlockCountCache.count() == 1 + + insert(:block, number: 2, consensus: true) + + Process.sleep(2_000) + + assert BlockCountCache.count() == 2 + end + + test "does not invalidate cache if period time did not pass" do + insert(:block, number: 1, consensus: true) + + Application.put_env(:explorer, BlockCountCache, ttl: 2_00) + BlockCountCache.setup() + + assert BlockCountCache.count() == 1 + + insert(:block, number: 2, consensus: true) + + assert BlockCountCache.count() == 1 + end + end +end