From d0009784af699b7b35da037a01cbeff63898560a Mon Sep 17 00:00:00 2001 From: slightlycyborg Date: Fri, 12 Jul 2019 10:35:35 +0000 Subject: [PATCH] Added Transaction.History.Historian and TransactionStats w/ migration --- .../chain/transaction/history/historian.ex | 53 +++++++++++ .../transaction/history/transaction_stats.ex | 37 ++++++++ ...0190709043832_create_transaction_stats.exs | 12 +++ .../transaction/history/historian_test.exs | 94 +++++++++++++++++++ .../history/transaction_stats_test.exs | 30 ++++++ 5 files changed, 226 insertions(+) create mode 100644 apps/explorer/lib/explorer/chain/transaction/history/historian.ex create mode 100644 apps/explorer/lib/explorer/chain/transaction/history/transaction_stats.ex create mode 100644 apps/explorer/priv/repo/migrations/20190709043832_create_transaction_stats.exs create mode 100644 apps/explorer/test/explorer/chain/transaction/history/historian_test.exs create mode 100644 apps/explorer/test/explorer/chain/transaction/history/transaction_stats_test.exs diff --git a/apps/explorer/lib/explorer/chain/transaction/history/historian.ex b/apps/explorer/lib/explorer/chain/transaction/history/historian.ex new file mode 100644 index 0000000000..dbe823328d --- /dev/null +++ b/apps/explorer/lib/explorer/chain/transaction/history/historian.ex @@ -0,0 +1,53 @@ +defmodule Explorer.Chain.Transaction.History.Historian do + use Explorer.History.Historian + alias Explorer.History.Process, as: HistoryProcess + alias Explorer.Repo + alias Explorer.Chain.Block + alias Explorer.Chain.Transaction.History.TransactionStats + + import Ecto.Query, only: [from: 2] + + alias Explorer.Chain.Transaction + + @behaviour Historian + + @impl Historian + def compile_records(num_days, records \\ []) do + + if num_days == 0 do + #base case + {:ok, records} + else + day_to_fetch = Date.add(date_today(), -1*(num_days-1)) + + earliest = datetime(day_to_fetch, ~T[00:00:00]) + latest = datetime(day_to_fetch, ~T[23:59:59]) + + query = from( + block in Block, + where: (block.timestamp >= ^earliest and block.timestamp <= ^latest), + join: transaction in Transaction, + on: block.hash == transaction.block_hash) + + num_transactions = Repo.aggregate query, :count, :hash + records = [%{date: day_to_fetch, number_of_transactions: num_transactions} | records ] + compile_records(num_days-1, records) + end + end + + @impl Historian + def save_records(records) do + {num_inserted, _} = Repo.insert_all(TransactionStats, records, on_conflict: :replace_all, conflict_target: [:date]) + num_inserted + end + + @spec datetime(Date.t(), Time.t()) :: DateTime.t() + defp datetime(date, time) do + {_success?, naive_dt} = NaiveDateTime.new(date, time) + DateTime.from_naive!(naive_dt, "Etc/UTC") + end + + defp date_today() do + HistoryProcess.config_or_default(:utc_today, Date.utc_today(), __MODULE__) + end +end diff --git a/apps/explorer/lib/explorer/chain/transaction/history/transaction_stats.ex b/apps/explorer/lib/explorer/chain/transaction/history/transaction_stats.ex new file mode 100644 index 0000000000..e50511646a --- /dev/null +++ b/apps/explorer/lib/explorer/chain/transaction/history/transaction_stats.ex @@ -0,0 +1,37 @@ +defmodule Explorer.Chain.Transaction.History.TransactionStats do + @moduledoc """ + Represents daily transaction numbers. + """ + + import Ecto.Query, only: [from: 2] + + use Explorer.Schema + + alias Explorer.Repo + + + schema "transaction_stats" do + field(:date, :date) + field(:number_of_transactions, :integer) + end + + @typedoc """ + The recorded values of the number of transactions for a single day. + * `:date` - The date in UTC. + * `:number_of_transactions` - Number of transactions processed by the vm for a given date. + """ + @type t :: %__MODULE__{ + date: Date.t(), + number_of_transactions: Integer.t() + } + + @spec by_date_range(Date.t(), Date.t()) :: [__MODULE__] + def by_date_range(earliest, latest) do + # Create a query + query = from stat in __MODULE__, + where: (stat.date >= ^earliest and stat.date<=^latest), + order_by: [desc: :date] + + Repo.all(query) + end +end diff --git a/apps/explorer/priv/repo/migrations/20190709043832_create_transaction_stats.exs b/apps/explorer/priv/repo/migrations/20190709043832_create_transaction_stats.exs new file mode 100644 index 0000000000..e358fc2e12 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20190709043832_create_transaction_stats.exs @@ -0,0 +1,12 @@ +defmodule Explorer.Repo.Migrations.CreateTransactionStats do + use Ecto.Migration + + def change do + create table(:transaction_stats) do + add(:date, :date) + add(:number_of_transactions, :integer) + end + + create(unique_index(:transaction_stats, :date)) + end +end diff --git a/apps/explorer/test/explorer/chain/transaction/history/historian_test.exs b/apps/explorer/test/explorer/chain/transaction/history/historian_test.exs new file mode 100644 index 0000000000..db43995c15 --- /dev/null +++ b/apps/explorer/test/explorer/chain/transaction/history/historian_test.exs @@ -0,0 +1,94 @@ +defmodule Explorer.Chain.Transaction.History.HistorianTest do + use Explorer.DataCase, async: false + + alias Explorer.Chain.Transaction.History.Historian + alias Explorer.Chain.Transaction.History.TransactionStats + + import Ecto.Query, only: [from: 2] + + + + setup do + Application.put_env(:explorer, Historian, utc_today: ~D[1970-01-04]) + :ok + end + + defp days_to_secs(days) do + 60*60*24*days + end + + describe "compile_records/1" do + + test "fetches transactions from blocks mined in the past num_days" do + + blocks = [ + #1970-01-03 00:00:60 + insert(:block, timestamp: DateTime.from_unix!(days_to_secs(2) + 60)), + + #1970-01-03 04:00:00 + insert(:block, timestamp: DateTime.from_unix!(days_to_secs(2) + (4*60*60))), + + #1970-01-02 00:00:00 + insert(:block, timestamp: DateTime.from_unix!(days_to_secs(1))) + ] + + insert(:transaction) |> with_block(Enum.at(blocks, 0)) + insert(:transaction) |> with_block(Enum.at(blocks, 1)) + insert(:transaction) |> with_block(Enum.at(blocks, 2)) + + expected = [ + %{date: ~D[1970-01-04], number_of_transactions: 0} + ] + assert {:ok, ^expected} = Historian.compile_records 1 + + + expected = [ + %{date: ~D[1970-01-04], number_of_transactions: 0}, + %{date: ~D[1970-01-03], number_of_transactions: 2}, + ] + assert {:ok, ^expected} = Historian.compile_records 2 + + + expected = [ + %{date: ~D[1970-01-04], number_of_transactions: 0}, + %{date: ~D[1970-01-03], number_of_transactions: 2}, + %{date: ~D[1970-01-02], number_of_transactions: 1} + ] + assert {:ok, ^expected} = Historian.compile_records 3 + end + end + + describe "save_records/1" do + test "saves transaction history records" do + records = [ + %{date: ~D[1970-01-04], number_of_transactions: 3}, + %{date: ~D[1970-01-03], number_of_transactions: 2}, + %{date: ~D[1970-01-02], number_of_transactions: 1} + ] + + Historian.save_records(records) + + query = from( + stats in TransactionStats, + select: %{date: stats.date, number_of_transactions: stats.number_of_transactions}, + order_by: [desc: stats.date]) + + results = Repo.all(query) + + assert 3 == length(results) + assert ^records = results + end + + test "overwrites records with the same date without error" do + records = [%{date: ~D[1970-01-04], number_of_transactions: 3}] + Historian.save_records(records) + records = [%{date: ~D[1970-01-04], number_of_transactions: 1}] + Historian.save_records(records) + end + end + + @tag capture_log: true + test "start_link" do + assert {:ok, _} = Historian.start_link([]) + end +end diff --git a/apps/explorer/test/explorer/chain/transaction/history/transaction_stats_test.exs b/apps/explorer/test/explorer/chain/transaction/history/transaction_stats_test.exs new file mode 100644 index 0000000000..b30143a450 --- /dev/null +++ b/apps/explorer/test/explorer/chain/transaction/history/transaction_stats_test.exs @@ -0,0 +1,30 @@ +defmodule Explorer.Chain.Transaction.History.TransactionStatsTest do + use Explorer.DataCase, async: false + + alias Explorer.Chain.Transaction.History.TransactionStats + alias Explorer.Repo + + test "by_date_range()" do + + some_transaction_stats = [%{date: ~D[2019-07-09], number_of_transactions: 10}, + %{date: ~D[2019-07-08], number_of_transactions: 20}, + %{date: ~D[2019-07-07], number_of_transactions: 30}] + + Repo.insert_all(TransactionStats, some_transaction_stats) + + all3 = TransactionStats.by_date_range(~D[2019-07-07], ~D[2019-07-09]) + assert 3 = length(all3) + + assert ~D[2019-07-09] = Enum.at(all3, 0).date + assert 10 == Enum.at(all3, 0).number_of_transactions + assert ~D[2019-07-08] = Enum.at(all3, 1).date + assert 20 == Enum.at(all3, 1).number_of_transactions + assert ~D[2019-07-07] = Enum.at(all3, 2).date + assert 30 == Enum.at(all3, 2).number_of_transactions + + + just2 = TransactionStats.by_date_range(~D[2019-07-08], ~D[2019-07-09]) + assert 2 == length(just2) + assert ~D[2019-07-08] = Enum.at(just2, 1).date + end +end