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

feat: poisson regression and gas price feature flag #1575

Closed
wants to merge 44 commits into from
Closed

Conversation

unnawut
Copy link
Contributor

@unnawut unnawut commented Jun 10, 2020

Relates to #1268

Overview

Adds a new gas estimation strategy using poisson regression (also used by ETH Gas Station).

With dual strategies, a feature flag BLOCK_SUBMIT_GAS_PRICE_STRATEGY is also introduced.

Changes

  • Refactor gas price adjustment in OMG.ChildChain.BlockQueue.Core into OMG.ChildChain.GasPrice and OMG.ChildChain.GasPrice.LegacyGasStrategy
  • Add OMG.ChildChain.GasPrice.PoissonGasStrategy
  • Add BLOCK_SUBMIT_GAS_PRICE_STRATEGY flag (options: LEGACY and POISSON, defaults to LEGACY).
  • Both LegacyGasStrategy and PoissonGasStrategy will still compute and log their results even with the flag. The flag is used specifically to dictate which strategy to use for actual block submission
  • Bump Ethereumex to 0.6.4 to get the customized url for batch requests

Testing

Gas price estimations from both LegacyGasStrategy and PoissonGasStrategy should appear in log.

@unnawut unnawut self-assigned this Jun 10, 2020
# See the License for the specific language governing permissions and
# limitations under the License.

defmodule OMG.ChildChain.BlockQueue.BlockSubmission do
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an untouched extraction from OMG.ChildChain.BlockQueue.Core

apps/omg_child_chain/lib/omg_child_chain/gas_price.ex Outdated Show resolved Hide resolved
# See the License for the specific language governing permissions and
# limitations under the License.

defmodule OMG.ChildChain.GasPrice.LegacyGasStrategy do
Copy link
Contributor Author

@unnawut unnawut Jun 10, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A heavy refactor of the gas estimation formally OMG.ChildChain.BlockQueue.Core.adjust_gas_price() and OMG.ChildChain.BlockQueue.Core.adjust_gas_price(). All states in BlockQueue.Core that were used solely for gas price estimation also got moved here.

I'm expecting this to be 1:1 parity in behaviour with the current gas price estimation

@impl GenServer
def handle_continue(:start_recalculate, state) do
_ = send(self(), :recalculate)
{:ok, _} = :timer.send_interval(@recalculate_interval_ms, self(), :recalculate)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was going to use the handle_info(:timeout, ... approach that @mederic-p shared with me, but this genserver will be accepting :get_price call regularly and so I think it will interfere with the timeout approach.


# The ets table is not initialized with `:read_concurrency` because we are expecting interleaving
# reads and writes. See http://erlang.org/doc/man/ets.html
history_ets = :ets.new(@history_table, [:ordered_set, :protected, :named_table])
Copy link
Contributor

@boolafish boolafish Jun 30, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[discussion/design]
When I was reading some let it crash related video, I saw a design recommendation that to isolate the owning process of ets, so the ets could still survive when the business logic handling process failed. In the case here, it is fetching data from network so potentially it could fail.

Okay, but I see you're already retrying there in Fetcher 😛 (I am hearing Ino coughing) so probably it would not bring too much benefit on that case. However, testing wise, it might still be very convenient to be able to inject a ets table.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isolate the owning process of ets, so the ets could still survive when the business logic handling process failed

Hmm I did not realize that this process will take owner ship of this ets, and couldn't find any mention in the docs? Do you have a link to something I can read? If not I'll try experiment it through iex.

However, testing wise, it might still be very convenient to be able to inject a ets table.

Yeah you are right on this. Will update.

Copy link
Contributor

@boolafish boolafish Jun 30, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably this shows the ownership of the ets table: https://dev.to/strech/where-is-my-ets-table-3ff1 (from my random googling :p I am not 100% sure too actually after reading your comment)

Here is the previous video and slide I mentioned above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And you are right!

iex(5)> task = Task.async(fn ->
...(5)>   table = :ets.new(:some_history_table, [:ordered_set, :protected, :named_table])
...(5)>   IO.inspect("#{inspect(self())}: ets table created: #{inspect(table)}")
...(5)>
...(5)>   record = {:foo, "bar"}
...(5)>   true = :ets.insert(table, record)
...(5)>   IO.inspect("#{inspect(self())}: inserted #{inspect(record)} to #{inspect(table)}")
...(5)> end)
"#PID<0.128.0>: ets table created: :some_history_table"
"#PID<0.128.0>: inserted {:foo, \"bar\"} to :some_history_table"
%Task{
  owner: #PID<0.105.0>,
  pid: #PID<0.128.0>,
  ref: #Reference<0.575725472.2248409092.246859>
}

iex(6)>
nil

iex(7)> _ = Task.await(task)
"#PID<0.128.0>: inserted {:foo, \"bar\"} to :some_history_table"

iex(8)>
nil

iex(9)> IO.inspect("#{inspect(self())}: The async task is: #{inspect(Process.info(task.pid))}")
"#PID<0.105.0>: The async task is: nil"
"#PID<0.105.0>: The async task is: nil"

iex(10)> IO.inspect("#{inspect(self())}: :some_history_table has #{inspect(:ets.tab2list(:some_history_table))}")
** (ArgumentError) argument error
    (stdlib 3.12) :ets.match_object(:some_history_table, :_)
    (stdlib 3.12) ets.erl:763: :ets.tab2list/1

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good points raised by both of you.
It's not uncommon to create the ETS table in the application process (top level supervisor) if the purpose of the ETS is to NOT crash it with the child process. Or, you put a process next to Server, let's call it Manager and you set the ETS configuration so that heir is the Manager process. In case Server restarts, it'll hand off the data to Manager. And implement the logic in the Manager that will give it back to the Server once it's restarted.

Don't forget, ETS is a shared state entity, so you need to be super careful. Because you might put something in there that will crash other processes (the reason why Erlang/Elixir are not pure functional languages is because they have primitives like ETS).

But what you've done here in Server and later in the Fetcher is that you're trying to handle everything that might happen to the process and prevent it from dying. Not everything (you could have trapped exits!).
The problem is... you can not be 100% in your approach. Remember the :hackney bug that uncontrolably sends a ssl_closed message? benoitc/hackney#464
This would crash your Server and the ETS with it.

]

_ = Logger.info("Starting #{__MODULE__}")
Supervisor.init(children, strategy: :one_for_one)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if History dies, all process need to restart to re-subscribe? Probably cannot use one_for_one?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow epic catch! 💯 💯 💯 💯 💯

Bouncing idea: Would keeping :one_for_one as is, and instead Process.link/1 the history and the strategy during History.subscribe/1 be more optimal?

Otherwise if we change to :all_for_one, we also kill LegacyGasStrategy which does not depend on History and unnecessarily destroys its state. And :rest_for_one depends on the children sequence which I think it a bit too implicit/mistake-prone.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc: @InoMurko for opinion

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds like a good idea! 🔗🔗🔗🔗🔗🔗

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what you wanted to do, but designing a proper pub/sub ... this might be a little bit too simplistic. For example, you're not tracking dead pids in your subscription list. Perhaps it would be easier to simply use OMG.Bus and define a new topic?

Copy link
Contributor

@InoMurko InoMurko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good progress, needs some polishing


# The ets table is not initialized with `:read_concurrency` because we are expecting interleaving
# reads and writes. See http://erlang.org/doc/man/ets.html
history_ets = :ets.new(@history_table, [:ordered_set, :protected, :named_table])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good points raised by both of you.
It's not uncommon to create the ETS table in the application process (top level supervisor) if the purpose of the ETS is to NOT crash it with the child process. Or, you put a process next to Server, let's call it Manager and you set the ETS configuration so that heir is the Manager process. In case Server restarts, it'll hand off the data to Manager. And implement the logic in the Manager that will give it back to the Server once it's restarted.

Don't forget, ETS is a shared state entity, so you need to be super careful. Because you might put something in there that will crash other processes (the reason why Erlang/Elixir are not pure functional languages is because they have primitives like ETS).

But what you've done here in Server and later in the Fetcher is that you're trying to handle everything that might happen to the process and prevent it from dying. Not everything (you could have trapped exits!).
The problem is... you can not be 100% in your approach. Remember the :hackney bug that uncontrolably sends a ssl_closed message? benoitc/hackney#464
This would crash your Server and the ETS with it.

_ = Logger.info("#{__MODULE__} available gas prices from Eth heights: #{from_height} - #{to_height}.")

# Inform all subscribers that the history has been updated.
_ = Enum.each(state.subscribers, fn subscriber -> send(subscriber, {History, :updated}) end)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how come didn't you utilise OMG.Bus for this?

prices = Enum.map(block["transactions"], fn tx -> Encoding.int_from_hex(tx["gasPrice"]) end)
timestamp = Encoding.int_from_hex(block["timestamp"])

true = :ets.insert(history_ets, {height, prices, timestamp})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not insert in bulk?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's already streaming per item though (because of the processing needed on each item). Not sure if it's useful to bulk it the stream up before inserting... 🤔

# See the License for the specific language governing permissions and
# limitations under the License.

defmodule OMG.ChildChain.GasPrice.History.Fetcher do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that you found the correct border - the logic that separates the integration point and your internal algorithm. Noice!
I would add another option that allows you to test this (wink, I noticed you didn't explicitly test this file).

I found it greatly simplifies testing if you use Ethereumex second/third Keyword paramter: url: url.
That allows you to define your own server and simplifies exunit testing considerably! Lemme know if you're interested how I've done it.

@impl GenServer
def handle_cast({:subscribe, subscriber}, state) do
subscribers = Enum.uniq([subscriber | state.subscribers])
{:noreply, %{state | subscribers: subscribers}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is an ever growing list of subscribers? how do you remove dead pids?

]

_ = Logger.info("Starting #{__MODULE__}")
Supervisor.init(children, strategy: :one_for_one)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what you wanted to do, but designing a proper pub/sub ... this might be a little bit too simplistic. For example, you're not tracking dead pids in your subscription list. Perhaps it would be easier to simply use OMG.Bus and define a new topic?

# Internal implementations
#

defp do_recalculate() do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps a quick desc in what it does.
I believe History.all() returns all stored blocks (like 200) and you find the median price?
What happens if you don't have enough blocks to properly estimate, could it even happen?

# See the License for the specific language governing permissions and
# limitations under the License.

defmodule OMG.ChildChain.GasPrice.Strategy.LegacyGasStrategy do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this didn't change right? I don't have to look at this? he he he

@unnawut unnawut marked this pull request as draft July 8, 2020 04:46
@unnawut
Copy link
Contributor Author

unnawut commented Jul 16, 2020

Deprioritized for now. Will pick back up maybe during the cleanup period

@unnawut unnawut closed this Jul 16, 2020
@unnawut
Copy link
Contributor Author

unnawut commented Aug 19, 2020

Let the genie out!

@unnawut
Copy link
Contributor Author

unnawut commented Aug 19, 2020

@unnawut
Copy link
Contributor Author

unnawut commented Sep 16, 2020

@unnawut unnawut closed this Sep 16, 2020
@InoMurko InoMurko deleted the unnawut/gas branch February 10, 2021 20:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants