Use `parity_pendingTransactions` JSONRPC to get the pending transactions and import them. If they conflict with pre-existing transactions, then the previous transactions win, under the assumption that sometimes pending transactions repository transaction will COMMIT after the the realtime index has COMMITed that same transaction being validated as the timeout for transactions is 60 seconds while the block rate is faster than that.pull/255/head
parent
e520b5d5bd
commit
d3ea7c251a
@ -0,0 +1,101 @@ |
|||||||
|
defmodule Explorer.Indexer.PendingTransactionFetcher do |
||||||
|
@moduledoc """ |
||||||
|
Fetches pending transactions and imports them. |
||||||
|
|
||||||
|
*NOTE*: Pending transactions are imported with with `on_conflict: :nothing`, so that they don't overwrite their own |
||||||
|
validated version that may make it to the database first. |
||||||
|
""" |
||||||
|
use GenServer |
||||||
|
|
||||||
|
require Logger |
||||||
|
|
||||||
|
import EthereumJSONRPC.Parity, only: [fetch_pending_transactions: 0] |
||||||
|
import Explorer.Indexer.AddressExtraction, only: [transactions_params_to_addresses_params: 1] |
||||||
|
|
||||||
|
alias Explorer.{Chain, Indexer} |
||||||
|
alias Explorer.Indexer.PendingTransactionFetcher |
||||||
|
|
||||||
|
# milliseconds |
||||||
|
@default_interval 1_000 |
||||||
|
|
||||||
|
defstruct interval: @default_interval, |
||||||
|
task_ref: nil, |
||||||
|
task_pid: nil |
||||||
|
|
||||||
|
@gen_server_options ~w(debug name spawn_opt timeout)a |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Starts the pending transaction fetcher. |
||||||
|
|
||||||
|
## Options |
||||||
|
|
||||||
|
* `:debug` - if present, the corresponding function in the [`:sys` module](http://www.erlang.org/doc/man/sys.html) |
||||||
|
is invoked |
||||||
|
* `:name` - used for name registration as described in the "Name registration" section of the `GenServer` module |
||||||
|
documentation |
||||||
|
* `:pending_transaction_interval` - the millisecond time between checking for pending transactions. Defaults to |
||||||
|
`#{@default_interval}` milliseconds. |
||||||
|
* `:spawn_opt` - if present, its value is passed as options to the underlying process as in `Process.spawn/4` |
||||||
|
* `:timeout` - if present, the server is allowed to spend the given number of milliseconds initializing or it will |
||||||
|
be terminated and the start function will return `{:error, :timeout}` |
||||||
|
|
||||||
|
""" |
||||||
|
def start_link(opts) do |
||||||
|
GenServer.start_link(__MODULE__, Keyword.drop(opts, @gen_server_options), Keyword.take(opts, @gen_server_options)) |
||||||
|
end |
||||||
|
|
||||||
|
@impl GenServer |
||||||
|
def init(opts) do |
||||||
|
opts = |
||||||
|
:explorer |
||||||
|
|> Application.fetch_env!(:indexer) |
||||||
|
|> Keyword.merge(opts) |
||||||
|
|
||||||
|
state = |
||||||
|
%PendingTransactionFetcher{interval: opts[:pending_transaction_interval] || @default_interval} |
||||||
|
|> schedule_fetch() |
||||||
|
|
||||||
|
{:ok, state} |
||||||
|
end |
||||||
|
|
||||||
|
@impl GenServer |
||||||
|
def handle_info(:fetch, %PendingTransactionFetcher{} = state) do |
||||||
|
{:ok, pid, ref} = Indexer.start_monitor(fn -> task(state) end) |
||||||
|
{:noreply, %PendingTransactionFetcher{state | task_ref: ref, task_pid: pid}} |
||||||
|
end |
||||||
|
|
||||||
|
def handle_info({:DOWN, ref, :process, pid, reason}, %PendingTransactionFetcher{task_ref: ref, task_pid: pid} = state) do |
||||||
|
case reason do |
||||||
|
:normal -> |
||||||
|
:ok |
||||||
|
|
||||||
|
_ -> |
||||||
|
Logger.error(fn -> "pending transaction fetcher task exited due to #{inspect(reason)}. Rescheduling." end) |
||||||
|
end |
||||||
|
|
||||||
|
new_state = |
||||||
|
%PendingTransactionFetcher{state | task_ref: nil, task_pid: nil} |
||||||
|
|> schedule_fetch() |
||||||
|
|
||||||
|
{:noreply, new_state} |
||||||
|
end |
||||||
|
|
||||||
|
defp schedule_fetch(%PendingTransactionFetcher{interval: interval} = state) do |
||||||
|
Process.send_after(self(), :fetch, interval) |
||||||
|
state |
||||||
|
end |
||||||
|
|
||||||
|
defp task(%PendingTransactionFetcher{} = _state) do |
||||||
|
{:ok, transactions_params} = fetch_pending_transactions() |
||||||
|
addresses_params = transactions_params_to_addresses_params(transactions_params) |
||||||
|
|
||||||
|
# There's no need to queue up fetching the address balance since theses are pending transactions and cannot have |
||||||
|
# affected the address balance yet since address balance is a balance at a give block and these transactions are |
||||||
|
# blockless. |
||||||
|
{:ok, _} = |
||||||
|
Chain.import_blocks( |
||||||
|
addresses: [params: addresses_params], |
||||||
|
transactions: [on_conflict: :nothing, params: transactions_params] |
||||||
|
) |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,23 @@ |
|||||||
|
defmodule Explorer.Indexer.PendingTransactionFetcherTest do |
||||||
|
# `async: false` due to use of named GenServer |
||||||
|
use Explorer.DataCase, async: false |
||||||
|
|
||||||
|
alias Explorer.Chain.Transaction |
||||||
|
alias Explorer.Indexer.PendingTransactionFetcher |
||||||
|
|
||||||
|
describe "start_link/1" do |
||||||
|
# this test may fail if Sokol so low volume that the pending transactions are empty for too long |
||||||
|
test "starts fetching pending transactions" do |
||||||
|
assert Repo.aggregate(Transaction, :count, :hash) == 0 |
||||||
|
|
||||||
|
start_supervised!({Task.Supervisor, name: Explorer.Indexer.TaskSupervisor}) |
||||||
|
start_supervised!(PendingTransactionFetcher) |
||||||
|
|
||||||
|
wait_for_results(fn -> |
||||||
|
Repo.one!(from(transaction in Transaction, where: is_nil(transaction.block_hash), limit: 1)) |
||||||
|
end) |
||||||
|
|
||||||
|
assert Repo.aggregate(Transaction, :count, :hash) >= 1 |
||||||
|
end |
||||||
|
end |
||||||
|
end |
Loading…
Reference in new issue