|
|
|
@ -2,7 +2,7 @@ defmodule Explorer.Chain.Address.Counters do |
|
|
|
|
@moduledoc """ |
|
|
|
|
Functions related to Explorer.Chain.Address counters |
|
|
|
|
""" |
|
|
|
|
import Ecto.Query, only: [from: 2, limit: 2, select: 3, subquery: 1, union: 2, where: 3] |
|
|
|
|
import Ecto.Query, only: [from: 2, limit: 2, select: 3, union: 2, where: 3] |
|
|
|
|
|
|
|
|
|
import Explorer.Chain, |
|
|
|
|
only: [select_repo: 1, wrapped_union_subquery: 1] |
|
|
|
@ -19,7 +19,6 @@ defmodule Explorer.Chain.Address.Counters do |
|
|
|
|
|
|
|
|
|
alias Explorer.Chain.{ |
|
|
|
|
Address, |
|
|
|
|
Address.CoinBalance, |
|
|
|
|
Address.CurrentTokenBalance, |
|
|
|
|
Block, |
|
|
|
|
Hash, |
|
|
|
@ -30,10 +29,17 @@ defmodule Explorer.Chain.Address.Counters do |
|
|
|
|
Withdrawal |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
alias Explorer.Chain.Cache.AddressesTabsCounters |
|
|
|
|
alias Explorer.Chain.Cache.Helper, as: CacheHelper |
|
|
|
|
|
|
|
|
|
require Logger |
|
|
|
|
|
|
|
|
|
@typep counter :: non_neg_integer() | nil |
|
|
|
|
|
|
|
|
|
@counters_limit 51 |
|
|
|
|
@types [:validations, :txs, :token_transfers, :token_balances, :logs, :withdrawals, :internal_txs] |
|
|
|
|
@txs_types [:txs_from, :txs_to, :txs_contract] |
|
|
|
|
|
|
|
|
|
defp address_hash_to_logs_query(address_hash) do |
|
|
|
|
from(l in Log, where: l.address_hash == ^address_hash) |
|
|
|
|
end |
|
|
|
@ -50,22 +56,6 @@ defmodule Explorer.Chain.Address.Counters do |
|
|
|
|
select_repo(options).exists?(address_hash_to_logs_query(address_hash)) |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
defp address_hash_to_coin_balances(address_hash) do |
|
|
|
|
query = |
|
|
|
|
from( |
|
|
|
|
cb in CoinBalance, |
|
|
|
|
where: cb.address_hash == ^address_hash, |
|
|
|
|
where: not is_nil(cb.value), |
|
|
|
|
select_merge: %{ |
|
|
|
|
delta: fragment("? - coalesce(lead(?, 1) over (order by ? desc), 0)", cb.value, cb.value, cb.block_number) |
|
|
|
|
} |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
from(balance in subquery(query), |
|
|
|
|
where: balance.delta != 0 |
|
|
|
|
) |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
def check_if_token_transfers_at_address(address_hash, options \\ []) do |
|
|
|
|
select_repo(options).exists?(from(tt in TokenTransfer, where: tt.from_address_hash == ^address_hash)) || |
|
|
|
|
select_repo(options).exists?(from(tt in TokenTransfer, where: tt.to_address_hash == ^address_hash)) |
|
|
|
@ -255,6 +245,39 @@ defmodule Explorer.Chain.Address.Counters do |
|
|
|
|
end |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
def address_hash_to_internal_txs_limited_count_query(address_hash) do |
|
|
|
|
query_to_address_hash_wrapped = |
|
|
|
|
InternalTransaction |
|
|
|
|
|> InternalTransaction.where_nonpending_block() |
|
|
|
|
|> InternalTransaction.where_address_fields_match(address_hash, :to_address_hash) |
|
|
|
|
|> InternalTransaction.where_is_different_from_parent_transaction() |
|
|
|
|
|> limit(@counters_limit) |
|
|
|
|
|> wrapped_union_subquery() |
|
|
|
|
|
|
|
|
|
query_from_address_hash_wrapped = |
|
|
|
|
InternalTransaction |
|
|
|
|
|> InternalTransaction.where_nonpending_block() |
|
|
|
|
|> InternalTransaction.where_address_fields_match(address_hash, :from_address_hash) |
|
|
|
|
|> InternalTransaction.where_is_different_from_parent_transaction() |
|
|
|
|
|> limit(@counters_limit) |
|
|
|
|
|> wrapped_union_subquery() |
|
|
|
|
|
|
|
|
|
query_created_contract_address_hash_wrapped = |
|
|
|
|
InternalTransaction |
|
|
|
|
|> InternalTransaction.where_nonpending_block() |
|
|
|
|
|> InternalTransaction.where_address_fields_match(address_hash, :created_contract_address_hash) |
|
|
|
|
|> InternalTransaction.where_is_different_from_parent_transaction() |
|
|
|
|
|> limit(@counters_limit) |
|
|
|
|
|> wrapped_union_subquery() |
|
|
|
|
|
|
|
|
|
query_to_address_hash_wrapped |
|
|
|
|
|> union(^query_from_address_hash_wrapped) |
|
|
|
|
|> union(^query_created_contract_address_hash_wrapped) |
|
|
|
|
|> wrapped_union_subquery() |
|
|
|
|
|> InternalTransaction.where_is_different_from_parent_transaction() |
|
|
|
|
|> limit(@counters_limit) |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
def address_counters(address, options \\ []) do |
|
|
|
|
validation_count_task = |
|
|
|
|
Task.async(fn -> |
|
|
|
@ -304,28 +327,33 @@ defmodule Explorer.Chain.Address.Counters do |
|
|
|
|
AddressTransactionsGasUsageCounter.fetch(address) |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
@counters_limit 51 |
|
|
|
|
|
|
|
|
|
@spec address_limited_counters(Hash.t(), Keyword.t()) :: |
|
|
|
|
{counter(), counter(), counter(), counter(), counter(), counter(), counter()} |
|
|
|
|
def address_limited_counters(address_hash, options) do |
|
|
|
|
cached_counters = |
|
|
|
|
Enum.reduce(@types, %{}, fn type, acc -> |
|
|
|
|
case AddressesTabsCounters.get_counter(type, address_hash) do |
|
|
|
|
{_datetime, counter, status} -> |
|
|
|
|
Map.put(acc, type, {status, counter}) |
|
|
|
|
|
|
|
|
|
_ -> |
|
|
|
|
acc |
|
|
|
|
end |
|
|
|
|
end) |
|
|
|
|
|
|
|
|
|
start = Time.utc_now() |
|
|
|
|
|
|
|
|
|
validations_count_task = |
|
|
|
|
Task.async(fn -> |
|
|
|
|
result = |
|
|
|
|
address_hash |
|
|
|
|
|> address_hash_to_validated_blocks_query() |
|
|
|
|
|> limit(@counters_limit) |
|
|
|
|
|> select_repo(options).aggregate(:count) |
|
|
|
|
|
|
|
|
|
Logger.info( |
|
|
|
|
"Time consumed for validations_count_task for #{address_hash} is #{Time.diff(Time.utc_now(), start, :millisecond)}ms" |
|
|
|
|
configure_task( |
|
|
|
|
:validations, |
|
|
|
|
cached_counters, |
|
|
|
|
address_hash_to_validated_blocks_query(address_hash), |
|
|
|
|
address_hash, |
|
|
|
|
options |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
result |
|
|
|
|
end) |
|
|
|
|
|
|
|
|
|
transactions_from_count_task = |
|
|
|
|
Task.async(fn -> |
|
|
|
|
run_or_ignore(cached_counters[:txs], :txs_from, address_hash, fn -> |
|
|
|
|
result = |
|
|
|
|
Transaction |
|
|
|
|
|> where([t], t.from_address_hash == ^address_hash) |
|
|
|
@ -338,11 +366,14 @@ defmodule Explorer.Chain.Address.Counters do |
|
|
|
|
"Time consumed for transactions_from_count_task for #{address_hash} is #{Time.diff(Time.utc_now(), start, :millisecond)}ms" |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
result |
|
|
|
|
AddressesTabsCounters.save_txs_counter_progress(address_hash, %{txs_types: [:txs_from], txs_from: result}) |
|
|
|
|
AddressesTabsCounters.drop_task(:txs_from, address_hash) |
|
|
|
|
|
|
|
|
|
{:txs_from, result} |
|
|
|
|
end) |
|
|
|
|
|
|
|
|
|
transactions_to_count_task = |
|
|
|
|
Task.async(fn -> |
|
|
|
|
run_or_ignore(cached_counters[:txs], :txs_to, address_hash, fn -> |
|
|
|
|
result = |
|
|
|
|
Transaction |
|
|
|
|
|> where([t], t.to_address_hash == ^address_hash) |
|
|
|
@ -355,11 +386,14 @@ defmodule Explorer.Chain.Address.Counters do |
|
|
|
|
"Time consumed for transactions_to_count_task for #{address_hash} is #{Time.diff(Time.utc_now(), start, :millisecond)}ms" |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
result |
|
|
|
|
AddressesTabsCounters.save_txs_counter_progress(address_hash, %{txs_types: [:txs_to], txs_to: result}) |
|
|
|
|
AddressesTabsCounters.drop_task(:txs_to, address_hash) |
|
|
|
|
|
|
|
|
|
{:txs_to, result} |
|
|
|
|
end) |
|
|
|
|
|
|
|
|
|
transactions_created_contract_count_task = |
|
|
|
|
Task.async(fn -> |
|
|
|
|
run_or_ignore(cached_counters[:txs], :txs_contract, address_hash, fn -> |
|
|
|
|
result = |
|
|
|
|
Transaction |
|
|
|
|
|> where([t], t.created_contract_address_hash == ^address_hash) |
|
|
|
@ -372,172 +406,166 @@ defmodule Explorer.Chain.Address.Counters do |
|
|
|
|
"Time consumed for transactions_created_contract_count_task for #{address_hash} is #{Time.diff(Time.utc_now(), start, :millisecond)}ms" |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
result |
|
|
|
|
end) |
|
|
|
|
|
|
|
|
|
token_transfer_count_task = |
|
|
|
|
Task.async(fn -> |
|
|
|
|
result = |
|
|
|
|
address_hash |
|
|
|
|
|> address_to_token_transfer_count_query() |
|
|
|
|
|> limit(@counters_limit) |
|
|
|
|
|> select_repo(options).aggregate(:count) |
|
|
|
|
AddressesTabsCounters.save_txs_counter_progress(address_hash, %{ |
|
|
|
|
txs_types: [:txs_contract], |
|
|
|
|
txs_contract: result |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
Logger.info( |
|
|
|
|
"Time consumed for token_transfer_count_task for #{address_hash} is #{Time.diff(Time.utc_now(), start, :millisecond)}ms" |
|
|
|
|
) |
|
|
|
|
AddressesTabsCounters.drop_task(:txs_contract, address_hash) |
|
|
|
|
|
|
|
|
|
result |
|
|
|
|
{:txs_contract, result} |
|
|
|
|
end) |
|
|
|
|
|
|
|
|
|
token_balances_count_task = |
|
|
|
|
Task.async(fn -> |
|
|
|
|
result = |
|
|
|
|
address_hash |
|
|
|
|
|> address_hash_to_token_balances_query() |
|
|
|
|
|> limit(@counters_limit) |
|
|
|
|
|> select_repo(options).aggregate(:count) |
|
|
|
|
|
|
|
|
|
Logger.info( |
|
|
|
|
"Time consumed for token_balances_count_task for #{address_hash} is #{Time.diff(Time.utc_now(), start, :millisecond)}ms" |
|
|
|
|
token_transfers_count_task = |
|
|
|
|
configure_task( |
|
|
|
|
:token_transfers, |
|
|
|
|
cached_counters, |
|
|
|
|
address_to_token_transfer_count_query(address_hash), |
|
|
|
|
address_hash, |
|
|
|
|
options |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
result |
|
|
|
|
end) |
|
|
|
|
token_balances_count_task = |
|
|
|
|
configure_task( |
|
|
|
|
:token_balances, |
|
|
|
|
cached_counters, |
|
|
|
|
address_hash_to_token_balances_query(address_hash), |
|
|
|
|
address_hash, |
|
|
|
|
options |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
logs_count_task = |
|
|
|
|
Task.async(fn -> |
|
|
|
|
result = |
|
|
|
|
address_hash |
|
|
|
|
|> address_hash_to_logs_query() |
|
|
|
|
|> limit(@counters_limit) |
|
|
|
|
|> select_repo(options).aggregate(:count) |
|
|
|
|
|
|
|
|
|
Logger.info( |
|
|
|
|
"Time consumed for logs_count_task for #{address_hash} is #{Time.diff(Time.utc_now(), start, :millisecond)}ms" |
|
|
|
|
configure_task( |
|
|
|
|
:logs, |
|
|
|
|
cached_counters, |
|
|
|
|
address_hash_to_logs_query(address_hash), |
|
|
|
|
address_hash, |
|
|
|
|
options |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
result |
|
|
|
|
end) |
|
|
|
|
|
|
|
|
|
withdrawals_count_task = |
|
|
|
|
Task.async(fn -> |
|
|
|
|
result = |
|
|
|
|
address_hash |
|
|
|
|
|> Withdrawal.address_hash_to_withdrawals_unordered_query() |
|
|
|
|
|> limit(@counters_limit) |
|
|
|
|
|> select_repo(options).aggregate(:count) |
|
|
|
|
|
|
|
|
|
Logger.info( |
|
|
|
|
"Time consumed for withdrawals_count_task for #{address_hash} is #{Time.diff(Time.utc_now(), start, :millisecond)}ms" |
|
|
|
|
configure_task( |
|
|
|
|
:withdrawals, |
|
|
|
|
cached_counters, |
|
|
|
|
Withdrawal.address_hash_to_withdrawals_unordered_query(address_hash), |
|
|
|
|
address_hash, |
|
|
|
|
options |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
result |
|
|
|
|
end) |
|
|
|
|
|
|
|
|
|
internal_txs_count_task = |
|
|
|
|
Task.async(fn -> |
|
|
|
|
query_to_address_hash_wrapped = |
|
|
|
|
InternalTransaction |
|
|
|
|
|> InternalTransaction.where_nonpending_block() |
|
|
|
|
|> InternalTransaction.where_address_fields_match(address_hash, :to_address_hash) |
|
|
|
|
|> InternalTransaction.where_is_different_from_parent_transaction() |
|
|
|
|
|> limit(@counters_limit) |
|
|
|
|
|> wrapped_union_subquery() |
|
|
|
|
|
|
|
|
|
query_from_address_hash_wrapped = |
|
|
|
|
InternalTransaction |
|
|
|
|
|> InternalTransaction.where_nonpending_block() |
|
|
|
|
|> InternalTransaction.where_address_fields_match(address_hash, :from_address_hash) |
|
|
|
|
|> InternalTransaction.where_is_different_from_parent_transaction() |
|
|
|
|
|> limit(@counters_limit) |
|
|
|
|
|> wrapped_union_subquery() |
|
|
|
|
|
|
|
|
|
query_created_contract_address_hash_wrapped = |
|
|
|
|
InternalTransaction |
|
|
|
|
|> InternalTransaction.where_nonpending_block() |
|
|
|
|
|> InternalTransaction.where_address_fields_match(address_hash, :created_contract_address_hash) |
|
|
|
|
|> InternalTransaction.where_is_different_from_parent_transaction() |
|
|
|
|
|> limit(@counters_limit) |
|
|
|
|
|> wrapped_union_subquery() |
|
|
|
|
|
|
|
|
|
result = |
|
|
|
|
query_to_address_hash_wrapped |
|
|
|
|
|> union(^query_from_address_hash_wrapped) |
|
|
|
|
|> union(^query_created_contract_address_hash_wrapped) |
|
|
|
|
|> wrapped_union_subquery() |
|
|
|
|
|> InternalTransaction.where_is_different_from_parent_transaction() |
|
|
|
|
|> limit(@counters_limit) |
|
|
|
|
|> select_repo(options).aggregate(:count) |
|
|
|
|
|
|
|
|
|
Logger.info( |
|
|
|
|
"Time consumed for internal_txs_count_task for #{address_hash} is #{Time.diff(Time.utc_now(), start, :millisecond)}ms" |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
result |
|
|
|
|
end) |
|
|
|
|
|
|
|
|
|
coin_balances_count_task = |
|
|
|
|
Task.async(fn -> |
|
|
|
|
result = |
|
|
|
|
address_hash |
|
|
|
|
|> address_hash_to_coin_balances() |
|
|
|
|
|> limit(@counters_limit) |
|
|
|
|
|> select_repo(options).aggregate(:count) |
|
|
|
|
|
|
|
|
|
Logger.info( |
|
|
|
|
"Time consumed for coin_balances_count_task for #{address_hash} is #{Time.diff(Time.utc_now(), start, :millisecond)}ms" |
|
|
|
|
configure_task( |
|
|
|
|
:internal_txs, |
|
|
|
|
cached_counters, |
|
|
|
|
address_hash_to_internal_txs_limited_count_query(address_hash), |
|
|
|
|
address_hash, |
|
|
|
|
options |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
result |
|
|
|
|
end) |
|
|
|
|
|
|
|
|
|
{validations, txs_from, txs_to, txs_contract, token_transfers, token_balances, logs, withdrawals, internal_txs, |
|
|
|
|
coin_balances} = |
|
|
|
|
map = |
|
|
|
|
[ |
|
|
|
|
validations_count_task, |
|
|
|
|
transactions_from_count_task, |
|
|
|
|
transactions_to_count_task, |
|
|
|
|
transactions_created_contract_count_task, |
|
|
|
|
token_transfer_count_task, |
|
|
|
|
token_transfers_count_task, |
|
|
|
|
token_balances_count_task, |
|
|
|
|
logs_count_task, |
|
|
|
|
withdrawals_count_task, |
|
|
|
|
internal_txs_count_task, |
|
|
|
|
coin_balances_count_task |
|
|
|
|
internal_txs_count_task |
|
|
|
|
] |
|
|
|
|
|> Task.yield_many(:timer.seconds(30)) |
|
|
|
|
|> Enum.map(fn {_task, res} -> |
|
|
|
|
|> Enum.reject(&is_nil/1) |
|
|
|
|
|> Task.yield_many(:timer.seconds(1)) |
|
|
|
|
|> Enum.reduce(Map.merge(prepare_cache_values(cached_counters), %{txs_types: [], txs_hashes: []}), fn {task, res}, |
|
|
|
|
acc -> |
|
|
|
|
case res do |
|
|
|
|
{:ok, result} -> |
|
|
|
|
result |
|
|
|
|
{:ok, {txs_type, txs_hashes}} when txs_type in @txs_types -> |
|
|
|
|
acc |
|
|
|
|
|> (&Map.put(&1, :txs_types, [txs_type | &1[:txs_types] || []])).() |
|
|
|
|
|> (&Map.put(&1, :txs_hashes, &1[:txs_hashes] ++ txs_hashes)).() |
|
|
|
|
|
|
|
|
|
{:ok, {type, counter}} -> |
|
|
|
|
Map.put(acc, type, counter) |
|
|
|
|
|
|
|
|
|
{:exit, reason} -> |
|
|
|
|
Logger.warn(fn -> |
|
|
|
|
[ |
|
|
|
|
"Query fetching address counters terminated: #{inspect(reason)}" |
|
|
|
|
"Query fetching address counters for #{address_hash} terminated: #{inspect(reason)}" |
|
|
|
|
] |
|
|
|
|
end) |
|
|
|
|
|
|
|
|
|
nil |
|
|
|
|
acc |
|
|
|
|
|
|
|
|
|
nil -> |
|
|
|
|
Logger.warn(fn -> |
|
|
|
|
[ |
|
|
|
|
"Query fetching address counters timed out." |
|
|
|
|
"Query fetching address counters for #{address_hash} timed out." |
|
|
|
|
] |
|
|
|
|
end) |
|
|
|
|
|
|
|
|
|
nil |
|
|
|
|
Task.ignore(task) |
|
|
|
|
|
|
|
|
|
acc |
|
|
|
|
end |
|
|
|
|
end) |
|
|
|
|
|> List.to_tuple() |
|
|
|
|
|> process_txs_counter() |
|
|
|
|
|
|
|
|
|
{map[:validations], map[:txs], map[:token_transfers], map[:token_balances], map[:logs], map[:withdrawals], |
|
|
|
|
map[:internal_txs]} |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
defp run_or_ignore({ok, _counter}, _type, _address_hash, _fun) when ok in [:up_to_date, :limit_value], do: nil |
|
|
|
|
|
|
|
|
|
defp run_or_ignore(_, type, address_hash, fun) do |
|
|
|
|
if !AddressesTabsCounters.get_task(type, address_hash) do |
|
|
|
|
AddressesTabsCounters.set_task(type, address_hash) |
|
|
|
|
|
|
|
|
|
{validations, |
|
|
|
|
(sanitize_list(txs_from) ++ sanitize_list(txs_to) ++ sanitize_list(txs_contract)) |> Enum.dedup() |> Enum.count(), |
|
|
|
|
token_transfers, token_balances, logs, withdrawals, internal_txs, coin_balances} |
|
|
|
|
Task.async(fun) |
|
|
|
|
end |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
defp configure_task(counter_type, cache, query, address_hash, options) do |
|
|
|
|
address_hash = to_string(address_hash) |
|
|
|
|
start = Time.utc_now() |
|
|
|
|
|
|
|
|
|
run_or_ignore(cache[counter_type], counter_type, address_hash, fn -> |
|
|
|
|
result = |
|
|
|
|
query |
|
|
|
|
|> limit(@counters_limit) |
|
|
|
|
|> select_repo(options).aggregate(:count) |
|
|
|
|
|
|
|
|
|
Logger.info( |
|
|
|
|
"Time consumed for #{counter_type} counter task for #{address_hash} is #{Time.diff(Time.utc_now(), start, :millisecond)}ms" |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
AddressesTabsCounters.set_counter(counter_type, address_hash, result) |
|
|
|
|
AddressesTabsCounters.drop_task(counter_type, address_hash) |
|
|
|
|
|
|
|
|
|
{counter_type, result} |
|
|
|
|
end) |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
defp process_txs_counter(%{txs_types: [_ | _] = txs_types, txs_hashes: hashes} = map) do |
|
|
|
|
counter = hashes |> Enum.uniq() |> Enum.count() |> min(@counters_limit) |
|
|
|
|
|
|
|
|
|
if Enum.count(txs_types) == 3 || counter == @counters_limit do |
|
|
|
|
map |> Map.put(:txs, counter) |
|
|
|
|
else |
|
|
|
|
map |
|
|
|
|
end |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
defp process_txs_counter(map), do: map |
|
|
|
|
|
|
|
|
|
defp prepare_cache_values(cached_counters) do |
|
|
|
|
Enum.reduce(cached_counters, %{}, fn |
|
|
|
|
{k, {_, counter}}, acc -> |
|
|
|
|
Map.put(acc, k, counter) |
|
|
|
|
|
|
|
|
|
{k, v}, acc -> |
|
|
|
|
Map.put(acc, k, v) |
|
|
|
|
end) |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
defp sanitize_list(nil), do: [] |
|
|
|
|
defp sanitize_list(other), do: other |
|
|
|
|
def txs_types, do: @txs_types |
|
|
|
|
def counters_limit, do: @counters_limit |
|
|
|
|
end |
|
|
|
|