parent
0d3264dcff
commit
357bcd5f04
@ -0,0 +1,62 @@ |
||||
defmodule Explorer.Utility.TokenTransferTokenIdMigratorProgress do |
||||
use Explorer.Schema |
||||
|
||||
require Logger |
||||
|
||||
alias Explorer.Chain.Cache.BlockNumber |
||||
alias Explorer.Repo |
||||
|
||||
schema "token_transfer_token_id_migrator_progress" do |
||||
field(:last_processed_block_number, :integer) |
||||
|
||||
timestamps() |
||||
end |
||||
|
||||
@doc false |
||||
def changeset(progress \\ %__MODULE__{}, params) do |
||||
cast(progress, params, [:last_processed_block_number]) |
||||
end |
||||
|
||||
def get_current_progress do |
||||
from( |
||||
p in __MODULE__, |
||||
order_by: [desc: p.updated_at], |
||||
limit: 1 |
||||
) |
||||
|> Repo.one() |
||||
end |
||||
|
||||
def get_last_processed_block_number do |
||||
case get_current_progress() do |
||||
nil -> |
||||
latest_processed_block_number = BlockNumber.get_max() + 1 |
||||
update_last_processed_block_number(latest_processed_block_number) |
||||
latest_processed_block_number |
||||
|
||||
%{last_processed_block_number: block_number} -> |
||||
block_number |
||||
end |
||||
end |
||||
|
||||
def update_last_processed_block_number(block_number) do |
||||
case get_current_progress() do |
||||
nil -> |
||||
%{last_processed_block_number: block_number} |
||||
|> changeset() |
||||
|> Repo.insert() |
||||
|
||||
progress -> |
||||
if progress.last_processed_block_number < block_number do |
||||
Logger.error( |
||||
"TokenTransferTokenIdMigratorProgress new block_number is above the last one. Last: #{progress.last_processed_block_number}, new: #{block_number}" |
||||
) |
||||
|
||||
{:error, :invalid_block_number} |
||||
else |
||||
progress |
||||
|> changeset(%{last_processed_block_number: block_number}) |
||||
|> Repo.update() |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,11 @@ |
||||
defmodule Explorer.Repo.Account.Migrations.CreateTokenTransferTokenIdMigratorProgress do |
||||
use Ecto.Migration |
||||
|
||||
def change do |
||||
create table(:token_transfer_token_id_migrator_progress) do |
||||
add(:last_processed_block_number, :integer) |
||||
|
||||
timestamps() |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,11 @@ |
||||
defmodule Explorer.Repo.Migrations.CreateTokenTransferTokenIdMigratorProgress do |
||||
use Ecto.Migration |
||||
|
||||
def change do |
||||
create table(:token_transfer_token_id_migrator_progress) do |
||||
add(:last_processed_block_number, :integer) |
||||
|
||||
timestamps() |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,61 @@ |
||||
defmodule Indexer.Fetcher.TokenTransferTokenIdMigration.LowestBlockNumberUpdater do |
||||
use GenServer |
||||
|
||||
alias Explorer.Utility.TokenTransferTokenIdMigratorProgress |
||||
|
||||
def start_link(_) do |
||||
GenServer.start_link(__MODULE__, :ok, name: __MODULE__) |
||||
end |
||||
|
||||
@impl true |
||||
def init(_) do |
||||
last_processed_block_number = TokenTransferTokenIdMigratorProgress.get_last_processed_block_number() |
||||
|
||||
{:ok, %{last_processed_block_number: last_processed_block_number, processed_ranges: []}} |
||||
end |
||||
|
||||
def add_range(from, to) do |
||||
GenServer.cast(__MODULE__, {:add_range, from..to}) |
||||
end |
||||
|
||||
@impl true |
||||
def handle_cast({:add_range, range}, %{processed_ranges: processed_ranges} = state) do |
||||
ranges = |
||||
[range | processed_ranges] |
||||
|> Enum.sort_by(& &1.last, &>=/2) |
||||
|> normalize_ranges() |
||||
|
||||
{new_last_number, new_ranges} = maybe_update_last_processed_number(state.last_processed_block_number, ranges) |
||||
|
||||
{:noreply, %{last_processed_block_number: new_last_number, processed_ranges: new_ranges}} |
||||
end |
||||
|
||||
defp normalize_ranges(ranges) do |
||||
%{prev_range: prev, result: result} = |
||||
Enum.reduce(ranges, %{prev_range: nil, result: []}, fn range, %{prev_range: prev_range, result: result} -> |
||||
case {prev_range, range} do |
||||
{nil, _} -> |
||||
%{prev_range: range, result: result} |
||||
|
||||
{%{first: f1, last: l1} = r1, %{first: f2, last: l2} = r2} -> |
||||
if l1 - 1 <= f2 do |
||||
%{prev_range: f1..l2, result: result} |
||||
else |
||||
%{prev_range: r2, result: result ++ [r1]} |
||||
end |
||||
end |
||||
end) |
||||
|
||||
result ++ [prev] |
||||
end |
||||
|
||||
# since ranges are normalized, we need to check only the first range to determine the new last_processed_number |
||||
defp maybe_update_last_processed_number(current_last, [from..to | rest] = ranges) when current_last - 1 <= from do |
||||
case TokenTransferTokenIdMigratorProgress.update_last_processed_block_number(to) do |
||||
{:ok, _} -> {to, rest} |
||||
_ -> {current_last, ranges} |
||||
end |
||||
end |
||||
|
||||
defp maybe_update_last_processed_number(current_last, ranges), do: {current_last, ranges} |
||||
end |
@ -0,0 +1,42 @@ |
||||
defmodule Indexer.Fetcher.TokenTransferTokenIdMigration.Supervisor do |
||||
use Supervisor |
||||
|
||||
alias Explorer.Utility.TokenTransferTokenIdMigratorProgress |
||||
alias Indexer.Fetcher.TokenTransferTokenIdMigration.LowestBlockNumberUpdater |
||||
alias Indexer.Fetcher.TokenTransferTokenIdMigration.Worker |
||||
|
||||
@default_first_block 0 |
||||
@default_workers_count 1 |
||||
|
||||
def start_link(_) do |
||||
Supervisor.start_link(__MODULE__, :ok, name: __MODULE__) |
||||
end |
||||
|
||||
@impl true |
||||
def init(_) do |
||||
first_block = Application.get_env(:indexer, :token_id_migration)[:first_block] || @default_first_block |
||||
last_block = TokenTransferTokenIdMigratorProgress.get_last_processed_block_number() |
||||
|
||||
if last_block > first_block do |
||||
workers_count = Application.get_env(:indexer, :token_id_migration)[:concurrency] || @default_workers_count |
||||
|
||||
workers = |
||||
Enum.map(1..workers_count, fn id -> |
||||
worker_name = build_worker_name(id) |
||||
|
||||
Supervisor.child_spec( |
||||
{Worker, |
||||
idx: id, first_block: first_block, last_block: last_block, step: workers_count - 1, name: worker_name}, |
||||
id: worker_name, |
||||
restart: :transient |
||||
) |
||||
end) |
||||
|
||||
Supervisor.init([LowestBlockNumberUpdater | workers], strategy: :one_for_one) |
||||
else |
||||
:ignore |
||||
end |
||||
end |
||||
|
||||
defp build_worker_name(worker_id), do: :"#{Worker}_#{worker_id}" |
||||
end |
@ -0,0 +1,79 @@ |
||||
defmodule Indexer.Fetcher.TokenTransferTokenIdMigration.Worker do |
||||
use GenServer |
||||
|
||||
import Ecto.Query |
||||
|
||||
alias Explorer.Chain.TokenTransfer |
||||
alias Explorer.Repo |
||||
alias Indexer.Fetcher.TokenTransferTokenIdMigration.LowestBlockNumberUpdater |
||||
|
||||
@default_batch_size 500 |
||||
@interval 10 |
||||
|
||||
def start_link(idx: idx, first_block: first, last_block: last, step: step, name: name) do |
||||
GenServer.start_link(__MODULE__, %{idx: idx, bottom_block: first, last_block: last, step: step}, name: name) |
||||
end |
||||
|
||||
@impl true |
||||
def init(%{idx: idx, bottom_block: bottom_block, last_block: last_block, step: step}) do |
||||
batch_size = Application.get_env(:indexer, :token_id_migration)[:batch_size] || @default_batch_size |
||||
range = calculate_new_range(last_block, bottom_block, batch_size, idx - 1) |
||||
|
||||
schedule_next_update() |
||||
|
||||
{:ok, %{batch_size: batch_size, bottom_block: bottom_block, step: step, current_range: range}} |
||||
end |
||||
|
||||
@impl true |
||||
def handle_info(:update, %{current_range: :out_of_bound} = state) do |
||||
{:stop, :normal, state} |
||||
end |
||||
|
||||
@impl true |
||||
def handle_info(:update, %{current_range: {lower_bound, upper_bound}} = state) do |
||||
case do_update(lower_bound, upper_bound) do |
||||
{_total, _result} -> |
||||
LowestBlockNumberUpdater.add_range(upper_bound, lower_bound) |
||||
new_range = calculate_new_range(lower_bound, state.bottom_block, state.batch_size, state.step) |
||||
schedule_next_update() |
||||
{:noreply, %{state | current_range: new_range}} |
||||
|
||||
_ -> |
||||
schedule_next_update() |
||||
{:noreply, state} |
||||
end |
||||
end |
||||
|
||||
defp calculate_new_range(last_processed_block, bottom_block, batch_size, step) do |
||||
upper_bound = last_processed_block - step * batch_size - 1 |
||||
lower_bound = max(upper_bound - batch_size + 1, bottom_block) |
||||
|
||||
if upper_bound >= bottom_block do |
||||
{lower_bound, upper_bound} |
||||
else |
||||
:out_of_bound |
||||
end |
||||
end |
||||
|
||||
defp do_update(lower_bound, upper_bound) do |
||||
query = |
||||
from( |
||||
tt in TokenTransfer, |
||||
where: tt.block_number >= ^lower_bound, |
||||
where: tt.block_number <= ^upper_bound, |
||||
where: not is_nil(tt.token_id), |
||||
update: [ |
||||
set: [ |
||||
token_ids: fragment("ARRAY_APPEND(ARRAY[]::decimal[], ?)", tt.token_id), |
||||
token_id: nil |
||||
] |
||||
] |
||||
) |
||||
|
||||
Repo.update_all(query, [], timeout: :infinity) |
||||
end |
||||
|
||||
defp schedule_next_update do |
||||
Process.send_after(self(), :update, @interval) |
||||
end |
||||
end |
@ -0,0 +1,37 @@ |
||||
defmodule Indexer.Fetcher.TokenTransferTokenIdMigration.LowestBlockNumberUpdaterTest do |
||||
use Explorer.DataCase |
||||
|
||||
alias Explorer.Repo |
||||
alias Explorer.Utility.TokenTransferTokenIdMigratorProgress |
||||
alias Indexer.Fetcher.TokenTransferTokenIdMigration.LowestBlockNumberUpdater |
||||
|
||||
describe "Add range and update last processed block number" do |
||||
test "add_range/2" do |
||||
TokenTransferTokenIdMigratorProgress.update_last_processed_block_number(2000) |
||||
LowestBlockNumberUpdater.start_link([]) |
||||
|
||||
LowestBlockNumberUpdater.add_range(1000, 500) |
||||
LowestBlockNumberUpdater.add_range(1500, 1001) |
||||
Process.sleep(10) |
||||
|
||||
assert %{last_processed_block_number: 2000, processed_ranges: [1500..500//-1]} = |
||||
:sys.get_state(LowestBlockNumberUpdater) |
||||
|
||||
assert %{last_processed_block_number: 2000} = Repo.one(TokenTransferTokenIdMigratorProgress) |
||||
|
||||
LowestBlockNumberUpdater.add_range(499, 300) |
||||
LowestBlockNumberUpdater.add_range(299, 0) |
||||
Process.sleep(10) |
||||
|
||||
assert %{last_processed_block_number: 2000, processed_ranges: [1500..0//-1]} = |
||||
:sys.get_state(LowestBlockNumberUpdater) |
||||
|
||||
assert %{last_processed_block_number: 2000} = Repo.one(TokenTransferTokenIdMigratorProgress) |
||||
|
||||
LowestBlockNumberUpdater.add_range(1999, 1501) |
||||
Process.sleep(10) |
||||
assert %{last_processed_block_number: 0, processed_ranges: []} = :sys.get_state(LowestBlockNumberUpdater) |
||||
assert %{last_processed_block_number: 0} = Repo.one(TokenTransferTokenIdMigratorProgress) |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,30 @@ |
||||
defmodule Indexer.Fetcher.TokenTransferTokenIdMigration.WorkerTest do |
||||
use Explorer.DataCase |
||||
|
||||
alias Explorer.Repo |
||||
alias Indexer.Fetcher.TokenTransferTokenIdMigration.Worker |
||||
alias Explorer.Utility.TokenTransferTokenIdMigratorProgress |
||||
alias Indexer.Fetcher.TokenTransferTokenIdMigration.LowestBlockNumberUpdater |
||||
|
||||
describe "Move TokenTransfer token_id to token_ids" do |
||||
test "Move token_ids and update last processed block number" do |
||||
insert(:token_transfer, block_number: 1, token_id: 1, transaction: insert(:transaction)) |
||||
insert(:token_transfer, block_number: 500, token_id: 2, transaction: insert(:transaction)) |
||||
insert(:token_transfer, block_number: 1000, token_id: 3, transaction: insert(:transaction)) |
||||
insert(:token_transfer, block_number: 1500, token_id: 4, transaction: insert(:transaction)) |
||||
insert(:token_transfer, block_number: 2000, token_id: 5, transaction: insert(:transaction)) |
||||
|
||||
TokenTransferTokenIdMigratorProgress.update_last_processed_block_number(3000) |
||||
LowestBlockNumberUpdater.start_link([]) |
||||
|
||||
Worker.start_link(idx: 1, first_block: 0, last_block: 3000, step: 0, name: :worker_name) |
||||
Process.sleep(200) |
||||
|
||||
token_transfers = Repo.all(Explorer.Chain.TokenTransfer) |
||||
assert Enum.all?(token_transfers, fn tt -> is_nil(tt.token_id) end) |
||||
|
||||
expected_token_ids = [[Decimal.new(1)], [Decimal.new(2)], [Decimal.new(3)], [Decimal.new(4)], [Decimal.new(5)]] |
||||
assert ^expected_token_ids = token_transfers |> Enum.map(& &1.token_ids) |> Enum.sort_by(&List.first/1) |
||||
end |
||||
end |
||||
end |
Loading…
Reference in new issue