diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v1/health_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v1/health_controller.ex index 1ca6bb53d4..1e65fa75ba 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v1/health_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v1/health_controller.ex @@ -1,31 +1,25 @@ defmodule BlockScoutWeb.API.V1.HealthController do use BlockScoutWeb, :controller - alias Explorer.{Chain, PagingOptions} + alias Explorer.Chain def health(conn, _) do - with {:ok, number, timestamp} <- Chain.last_block_status() do - send_resp(conn, :ok, result(number, timestamp)) + with {:ok, number, timestamp} <- Chain.last_db_block_status(), + {:ok, cache_number, cache_timestamp} <- Chain.last_cache_block_status() do + send_resp(conn, :ok, result(number, timestamp, cache_number, cache_timestamp)) else status -> send_resp(conn, :internal_server_error, error(status)) end end - def result(number, timestamp) do - latest_block_in_cache = - [ - paging_options: %PagingOptions{page_size: 1} - ] - |> Chain.list_blocks() - |> List.last() - + def result(number, timestamp, cache_number, cache_timestamp) do %{ "healthy" => true, "data" => %{ - "db_latest_block_number" => to_string(number), - "db_latest_block_inserted_at" => to_string(timestamp), - "cache_latest_block_number" => to_string(latest_block_in_cache.number), - "cache_latest_block_inserted_at" => to_string(latest_block_in_cache.timestamp) + "latest_block_number" => to_string(number), + "latest_block_inserted_at" => to_string(timestamp), + "cache_latest_block_number" => to_string(cache_number), + "cache_latest_block_inserted_at" => to_string(cache_timestamp) } } |> Jason.encode!() @@ -49,8 +43,8 @@ defmodule BlockScoutWeb.API.V1.HealthController do "error_description" => "There are no new blocks in the DB for the last 5 mins. Check the healthiness of Ethereum archive node or the Blockscout DB instance", "data" => %{ - "db_latest_block_number" => to_string(number), - "db_latest_block_inserted_at" => to_string(timestamp) + "latest_block_number" => to_string(number), + "latest_block_inserted_at" => to_string(timestamp) } } |> Jason.encode!() diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v1/health_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v1/health_controller_test.exs index 6b9db74208..19631ff746 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v1/health_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v1/health_controller_test.exs @@ -1,6 +1,15 @@ defmodule BlockScoutWeb.API.V1.HealthControllerTest do use BlockScoutWeb.ConnCase + alias Explorer.{Chain, PagingOptions} + + setup do + Supervisor.terminate_child(Explorer.Supervisor, {ConCache, Explorer.Chain.Cache.Blocks.cache_name()}) + Supervisor.restart_child(Explorer.Supervisor, {ConCache, Explorer.Chain.Cache.Blocks.cache_name()}) + + :ok + end + describe "GET last_block_status/0" do test "returns error when there are no blocks in db", %{conn: conn} do request = get(conn, api_v1_health_path(conn, :health)) @@ -25,8 +34,8 @@ defmodule BlockScoutWeb.API.V1.HealthControllerTest do "error_description" => "There are no new blocks in the DB for the last 5 mins. Check the healthiness of Ethereum archive node or the Blockscout DB instance", "data" => %{ - "db_latest_block_number" => _, - "db_latest_block_inserted_at" => _ + "latest_block_number" => _, + "latest_block_inserted_at" => _ } } = Poison.decode!(request.resp_body) end @@ -44,11 +53,38 @@ defmodule BlockScoutWeb.API.V1.HealthControllerTest do assert result["healthy"] == true assert %{ - "db_latest_block_number" => to_string(block1.number), - "db_latest_block_inserted_at" => to_string(block1.timestamp), + "latest_block_number" => to_string(block1.number), + "latest_block_inserted_at" => to_string(block1.timestamp), "cache_latest_block_number" => to_string(block1.number), "cache_latest_block_inserted_at" => to_string(block1.timestamp) } == result["data"] end end + + test "return error when cache is stale", %{conn: conn} do + stale_block = insert(:block, consensus: true, timestamp: Timex.shift(DateTime.utc_now(), hours: -50), number: 3) + state_block_hash = stale_block.hash + + assert [%{hash: ^state_block_hash}] = Chain.list_blocks(paging_options: %PagingOptions{page_size: 1}) + + insert(:block, consensus: true, timestamp: DateTime.utc_now(), number: 1) + + assert [%{hash: ^state_block_hash}] = Chain.list_blocks(paging_options: %PagingOptions{page_size: 1}) + + request = get(conn, api_v1_health_path(conn, :health)) + + assert request.status == 500 + + assert %{ + "healthy" => false, + "error_code" => 5001, + "error_title" => "blocks fetching is stuck", + "error_description" => + "There are no new blocks in the DB for the last 5 mins. Check the healthiness of Ethereum archive node or the Blockscout DB instance", + "data" => %{ + "latest_block_number" => _, + "latest_block_inserted_at" => _ + } + } = Poison.decode!(request.resp_body) + end end diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 0e01212c37..db545e1f9d 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -1818,7 +1818,7 @@ defmodule Explorer.Chain do Repo.one!(query) end - def last_block_status do + def last_db_block_status do query = from(block in Block, select: {block.number, block.timestamp}, @@ -1827,22 +1827,39 @@ defmodule Explorer.Chain do limit: 1 ) - case Repo.one(query) do - nil -> - {:error, :no_blocks} + query + |> Repo.one() + |> block_status() + end - {number, timestamp} -> - now = DateTime.utc_now() - last_block_period = DateTime.diff(now, timestamp, :millisecond) + def last_cache_block_status do + [ + paging_options: %PagingOptions{page_size: 1} + ] + |> list_blocks() + |> List.last() + |> case do + %{timestamp: timestamp, number: number} -> + block_status({number, timestamp}) - if last_block_period > Application.get_env(:explorer, :healthy_blocks_period) do - {:error, number, timestamp} - else - {:ok, number, timestamp} - end + _ -> + block_status(nil) end end + defp block_status({number, timestamp}) do + now = DateTime.utc_now() + last_block_period = DateTime.diff(now, timestamp, :millisecond) + + if last_block_period > Application.get_env(:explorer, :healthy_blocks_period) do + {:error, number, timestamp} + else + {:ok, number, timestamp} + end + end + + defp block_status(nil), do: {:error, :no_blocks} + @doc """ Calculates the ranges of missing consensus blocks in `range`. diff --git a/apps/explorer/test/explorer/chain_test.exs b/apps/explorer/test/explorer/chain_test.exs index 5ee506982b..234252dbe4 100644 --- a/apps/explorer/test/explorer/chain_test.exs +++ b/apps/explorer/test/explorer/chain_test.exs @@ -50,21 +50,35 @@ defmodule Explorer.ChainTest do end end - describe "last_block_status/0" do + describe "last_db_block_status/0" do test "return no_blocks errors if db is empty" do - assert {:error, :no_blocks} = Chain.last_block_status() + assert {:error, :no_blocks} = Chain.last_db_block_status() end test "returns {:ok, last_block_period} if block is in healthy period" do insert(:block, consensus: true) - assert {:ok, _, _} = Chain.last_block_status() + assert {:ok, _, _} = Chain.last_db_block_status() end test "return {:ok, last_block_period} if block is not in healthy period" do insert(:block, consensus: true, timestamp: Timex.shift(DateTime.utc_now(), hours: -50)) - assert {:error, _, _} = Chain.last_block_status() + assert {:error, _, _} = Chain.last_db_block_status() + end + end + + describe "last_cache_block_status/0" do + test "returns success if cache is not stale" do + insert(:block, consensus: true) + + assert {:ok, _, _} = Chain.last_cache_block_status() + end + + test "return error if cache is stale" do + insert(:block, consensus: true, timestamp: Timex.shift(DateTime.utc_now(), hours: -50)) + + assert {:error, _, _} = Chain.last_cache_block_status() end end