Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Smart Contracts: Do not schedule an interval tick if there is a datetime tick at the same time #1015

Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 39 additions & 13 deletions lib/archethic/contracts/worker.ex
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ defmodule Archethic.Contracts.Worker do
alias Archethic.Utils
alias Archethic.Utils.DetectNodeResponsiveness

alias Crontab.CronExpression.Parser, as: CronParser

@extended_mode? Mix.env() != :prod

require Logger
Expand Down Expand Up @@ -60,9 +62,11 @@ defmodule Archethic.Contracts.Worker do
end

def handle_continue(:start_schedulers, state = %{contract: %Contract{triggers: triggers}}) do
triggers_type = Map.keys(triggers)

new_state =
Enum.reduce(triggers, state, fn {trigger_type, _}, acc ->
case schedule_trigger(trigger_type) do
Enum.reduce(triggers_type, state, fn trigger_type, acc ->
case schedule_trigger(trigger_type, triggers_type) do
timer when is_reference(timer) ->
Map.update(acc, :timers, %{trigger_type => timer}, &Map.put(&1, trigger_type, timer))

Expand All @@ -82,7 +86,7 @@ defmodule Archethic.Contracts.Worker do
contract_tx = Constants.to_transaction(contract.constants.contract)

meta = log_metadata(contract_tx, trigger_tx)
Logger.debug("Contract execution started", meta)
Logger.debug("Contract execution started (trigger=transaction)", meta)

with true <- enough_funds?(contract_tx.address),
{:ok, calls} <- TransactionChain.fetch_contract_calls(contract_tx.address),
Expand Down Expand Up @@ -118,7 +122,7 @@ defmodule Archethic.Contracts.Worker do
contract_tx = Constants.to_transaction(contract.constants.contract)

meta = log_metadata(contract_tx)
Logger.debug("Contract execution started", meta)
Logger.debug("Contract execution started (trigger=datetime)", meta)

with true <- enough_funds?(contract_tx.address),
{:ok, calls} <- TransactionChain.fetch_contract_calls(contract_tx.address),
Expand All @@ -142,12 +146,12 @@ defmodule Archethic.Contracts.Worker do
# TRIGGER: INTERVAL
def handle_info(
{:trigger, trigger_type = {:interval, interval}},
state = %{contract: contract}
state = %{contract: contract = %Contract{triggers: triggers}}
) do
contract_tx = Constants.to_transaction(contract.constants.contract)

meta = log_metadata(contract_tx)
Logger.debug("Contract execution started", meta)
Logger.debug("Contract execution started (trigger=interval)", meta)

with true <- enough_funds?(contract_tx.address),
{:ok, calls} <- TransactionChain.fetch_contract_calls(contract_tx.address),
Expand All @@ -165,7 +169,7 @@ defmodule Archethic.Contracts.Worker do
Logger.debug("Contract execution failed", meta)
end

interval_timer = schedule_trigger({:interval, interval})
interval_timer = schedule_trigger({:interval, interval}, Map.keys(triggers))
{:noreply, put_in(state, [:timers, :interval], interval_timer)}
end

Expand All @@ -178,7 +182,7 @@ defmodule Archethic.Contracts.Worker do
{:ok, oracle_tx} = TransactionChain.get_transaction(tx_address)

meta = log_metadata(contract_tx, oracle_tx)
Logger.debug("Contract execution started", meta)
Logger.debug("Contract execution started (trigger=oracle)", meta)

with true <- enough_funds?(contract_tx.address),
{:ok, calls} <- TransactionChain.fetch_contract_calls(contract_tx.address),
Expand Down Expand Up @@ -208,27 +212,49 @@ defmodule Archethic.Contracts.Worker do
{:via, Registry, {ContractRegistry, address}}
end

defp schedule_trigger(trigger = {:interval, interval}) do
defp schedule_trigger(trigger = {:interval, interval}, triggers_type) do
now = DateTime.utc_now()

next_tick =
interval
|> CronParser.parse!(@extended_mode?)
|> Utils.next_date(now)

# do not allow an interval trigger if there is a datetime trigger at same time
# because one of them would get a "transaction is already mining"
next_tick =
if {:datetime, next_tick} in triggers_type do
Logger.debug(
"Contract scheduler skips next tick for trigger=interval because there is a trigger=datetime at the same time that takes precedence"
)

interval
|> CronParser.parse!(@extended_mode?)
|> Utils.next_date(next_tick)
else
next_tick
end

Process.send_after(
self(),
{:trigger, trigger},
Utils.time_offset(interval, DateTime.utc_now(), @extended_mode?) * 1000
DateTime.diff(next_tick, now, :millisecond)
)
end

defp schedule_trigger(trigger = {:datetime, datetime = %DateTime{}}) do
defp schedule_trigger(trigger = {:datetime, datetime = %DateTime{}}, _triggers_type) do
seconds = DateTime.diff(datetime, DateTime.utc_now())

if seconds > 0 do
Process.send_after(self(), {:trigger, trigger}, seconds * 1000)
end
end

defp schedule_trigger(:oracle) do
defp schedule_trigger(:oracle, _triggers_type) do
PubSub.register_to_new_transaction_by_type(:oracle)
end

defp schedule_trigger(_), do: :ok
defp schedule_trigger(_trigger_type, _triggers_type), do: :ok

defp handle_new_transaction(next_transaction = %Transaction{}) do
validation_nodes = get_validation_nodes(next_transaction)
Expand Down
Loading