Merge pull request #572 from poanetwork/532
Use WebSocket subscription to newHeads for realtime indexerpull/574/merge
commit
050870f8b4
@ -0,0 +1,5 @@ |
||||
# Tests with everything using `Mox` |
||||
|
||||
use Mix.Config |
||||
|
||||
config :ethereum_jsonrpc, EthereumJSONRPC.Case, json_rpc_named_arguments: [transport: EthereumJSONRPC.Mox] |
@ -0,0 +1,59 @@ |
||||
defmodule EthereumJSONRPC.Subscription do |
||||
@moduledoc """ |
||||
A subscription to an event |
||||
""" |
||||
|
||||
alias EthereumJSONRPC.Transport |
||||
|
||||
@enforce_keys ~w(id subscriber_pid transport transport_options)a |
||||
defstruct ~w(id subscriber_pid transport transport_options)a |
||||
|
||||
@typedoc """ |
||||
An event that can be suscribed to. |
||||
|
||||
* `"newHeads"` - when new blocks are added to chain including during reorgs. |
||||
""" |
||||
@type event :: String.t() |
||||
|
||||
@typedoc """ |
||||
Subscription ID returned from `eth_subscribe` and used to canceled a subscription with `eth_unsubscribe`. |
||||
""" |
||||
@type id :: String.t() |
||||
|
||||
@typedoc """ |
||||
Parameters for customizing subscription to `t:event/0`. |
||||
""" |
||||
@type params :: list() |
||||
|
||||
@typedoc """ |
||||
* `id` - the `t:/id/0` of the subscription on the server |
||||
* `subscriber_pid` - the `t:pid/0` of process where `transport_pid` should send messages |
||||
* `transport` - the `t:EthereumJSONRPC.Transport.t/0` callback module |
||||
* `transport_options` - options passed to `c:EthereumJSONRPC.Transport.json_rpc/2` |
||||
""" |
||||
@type t :: %__MODULE__{id: id, subscriber_pid: pid, transport: Transport.t(), transport_options: Transport.options()} |
||||
|
||||
@doc """ |
||||
Publishes `messages` to all `subscriptions`s' `subscriber_pid`s. |
||||
|
||||
Sends `message` tagged with each `subscription`: `{subscription, message}`. |
||||
""" |
||||
@spec broadcast(Enumerable.t(), message :: term()) :: :ok |
||||
def broadcast(subscriptions, message) do |
||||
Enum.each(subscriptions, &publish(&1, message)) |
||||
end |
||||
|
||||
@doc """ |
||||
Publishes `message` to the `subscription`'s `subscriber_pid`. |
||||
|
||||
Sends `message` tagged with `subscription`: `{subscription, message}`. |
||||
""" |
||||
@spec publish(t(), message :: term()) :: :ok |
||||
def publish(%__MODULE__{subscriber_pid: subscriber_pid} = subscription, message) do |
||||
send(subscriber_pid, subscription_message(subscription, message)) |
||||
end |
||||
|
||||
defp subscription_message(%__MODULE__{} = subscription, message) do |
||||
{subscription, message} |
||||
end |
||||
end |
@ -0,0 +1,113 @@ |
||||
defmodule EthereumJSONRPC.WebSocket do |
||||
@moduledoc """ |
||||
JSONRPC over WebSocket. |
||||
""" |
||||
|
||||
alias EthereumJSONRPC.{Subscription, Transport} |
||||
|
||||
@behaviour Transport |
||||
|
||||
@typedoc """ |
||||
WebSocket name |
||||
""" |
||||
# same as `t:GenServer.name/0` |
||||
@type name :: atom() | {:global, term()} | {:via, module(), term()} |
||||
|
||||
@typedoc """ |
||||
WebSocket reference |
||||
""" |
||||
# same as `t:GenServer.server/0` |
||||
@type web_socket :: pid() | name() | {atom(), node()} |
||||
|
||||
@typedoc """ |
||||
Options passed to `EthereumJSONRPC.Transport` callbacks. |
||||
|
||||
**MUST** contain `t:web_socket/0` referring to `t:pid/0` returned by `c:start_link/2`. |
||||
""" |
||||
@type options :: [{:web_socket, web_socket()} | {:web_socket_options, term()}] |
||||
|
||||
@doc """ |
||||
Allow `c:start_link/1` to be called as part of a supervision tree. |
||||
""" |
||||
@callback child_spec([url :: String.t() | options :: term()]) :: Supervisor.child_spec() |
||||
|
||||
@doc """ |
||||
Starts web socket attached to `url` with `options`. |
||||
|
||||
|
||||
""" |
||||
# Return is same as `t:GenServer.on_start/0` |
||||
@callback start_link([url :: String.t() | options :: term()]) :: |
||||
{:ok, pid()} | :ignore | {:error, {:already_started, pid()} | reason :: term()} |
||||
|
||||
@doc """ |
||||
Run a single Remote Procedure Call (RPC) `t:EthereumJSONRPC.Transport.request/0` through `t:web_socket/0`. |
||||
|
||||
## Returns |
||||
|
||||
* `{:ok, result}` - `result` is the `/result` from JSONRPC response object of format |
||||
`%{"id" => ..., "result" => result}`. |
||||
* `{:error, reason}` - `reason` is the the `/error` from JSONRPC response object of format |
||||
`%{"id" => ..., "error" => reason}`. The transport can also give any `term()` for `reason` if a more specific |
||||
reason is possible. |
||||
|
||||
""" |
||||
@callback json_rpc(web_socket(), Transport.request()) :: {:ok, Transport.result()} | {:error, reason :: term()} |
||||
|
||||
@doc """ |
||||
Subscribes to `t:EthereumJSONRPC.Subscription.event/0` with `t:EthereumJSONRPC.Subscription.params/0` through |
||||
`t:web_socket/0`. |
||||
|
||||
Events are delivered in a tuple tagged with the `t:EthereumJSONRPC.Subscription.t/0` and containing the same output |
||||
as `json_rpc/2`. |
||||
|
||||
| Message | Description | |
||||
|-----------------------------------------------------------------------------------|----------------------------------------| |
||||
| `{EthereumJSONRPC.Subscription.t(), {:ok, EthreumsJSONRPC.Transport.result.t()}}` | New result in subscription | |
||||
| `{EthereumJSONRPC.Subscription.t(), {:error, reason :: term()}}` | There was an error in the subscription | |
||||
|
||||
Subscription can be canceled by calling `unsubscribe/1` with the returned `t:EthereumJSONRPC.Subscription.t/0`. |
||||
""" |
||||
@callback subscribe(web_socket(), event :: Subscription.event(), params :: Subscription.params()) :: |
||||
{:ok, Subscription.t()} | {:error, reason :: term()} |
||||
|
||||
@doc """ |
||||
Unsubscribes to `t:EthereumJSONRPC.Subscription.t/0` created with `subscribe/2`. |
||||
|
||||
## Returns |
||||
|
||||
* `:ok` - subscription was canceled |
||||
* `{:error, reason}` - subscription could not be canceled. |
||||
|
||||
""" |
||||
@callback unsubscribe(web_socket(), Subscription.t()) :: :ok | {:error, reason :: term()} |
||||
|
||||
@impl Transport |
||||
@spec json_rpc(Transport.request(), options) :: {:ok, Transport.result()} | {:error, reason :: term()} |
||||
def json_rpc(request, options) do |
||||
web_socket_module = Keyword.fetch!(options, :web_socket) |
||||
%{web_socket: web_socket} = Keyword.fetch!(options, :web_socket_options) |
||||
|
||||
web_socket_module.json_rpc(web_socket, request) |
||||
end |
||||
|
||||
@impl Transport |
||||
@spec subscribe(event :: Subscription.event(), params :: Subscription.params(), options) :: |
||||
{:ok, Subscription.t()} | {:error, reason :: term()} |
||||
def subscribe(event, params, options) when is_binary(event) and is_list(params) do |
||||
web_socket_module = Keyword.fetch!(options, :web_socket) |
||||
%{web_socket: web_socket} = Keyword.fetch!(options, :web_socket_options) |
||||
|
||||
web_socket_module.subscribe(web_socket, event, params) |
||||
end |
||||
|
||||
@impl Transport |
||||
@spec unsubscribe(%Subscription{transport: __MODULE__, transport_options: options}) :: |
||||
:ok | {:error, reason :: term()} |
||||
def unsubscribe(%Subscription{transport: __MODULE__, transport_options: transport_options} = subscription) do |
||||
web_socket_module = Keyword.fetch!(transport_options, :web_socket) |
||||
%{web_socket: web_socket} = Keyword.fetch!(transport_options, :web_socket_options) |
||||
|
||||
web_socket_module.unsubscribe(web_socket, subscription) |
||||
end |
||||
end |
@ -0,0 +1,24 @@ |
||||
defmodule EthereumJSONRPC.WebSocket.Registration do |
||||
@moduledoc """ |
||||
When a caller registers for responses to asynchronous frame responses. |
||||
""" |
||||
|
||||
alias EthereumJSONRPC.Subscription |
||||
|
||||
@enforce_keys ~w(from type)a |
||||
defstruct ~w(from type subscription_id)a |
||||
|
||||
@typedoc """ |
||||
What kind of request will be issued by the caller |
||||
|
||||
* `:json_rpc` - a generic JSONRPC request that just needs to be returned to the caller based on `id` matching. |
||||
* `:subscribe` - an `eth_subscribe` request will be issued by the caller. Its response need to be returned to |
||||
caller **AND** the client needs to `EthereumsJSONRPC.Subscription.publish/2` any `eth_subscription` messages to |
||||
the caller until the `EthereumJSONRPC.WebSocket.Client.unsubscribe/1` is called. |
||||
* `:unsubscribe` - an `eth_unsubscribe` request will be issued by the caller. Its response needs to be returned to |
||||
caller **AND** the client needs to stop tracking the subscription. |
||||
""" |
||||
@type type :: :json_rpc | :subscribe | :unsubscribe |
||||
|
||||
@type t :: %__MODULE__{from: GenServer.from(), type: type, subscription_id: Subscription.id()} |
||||
end |
@ -0,0 +1,322 @@ |
||||
defmodule EthereumJSONRPC.WebSocket.WebSocketClient do |
||||
@moduledoc """ |
||||
`EthereumJSONRPC.WebSocket` that uses `websocket_client` |
||||
""" |
||||
|
||||
require Logger |
||||
|
||||
import EthereumJSONRPC, only: [request: 1] |
||||
|
||||
alias EthereumJSONRPC.{Subscription, Transport, WebSocket} |
||||
alias EthereumJSONRPC.WebSocket.Registration |
||||
|
||||
@behaviour :websocket_client |
||||
@behaviour WebSocket |
||||
|
||||
@enforce_keys ~w(url)a |
||||
defstruct request_id_to_registration: %{}, |
||||
subscription_id_to_subscription: %{}, |
||||
url: nil |
||||
|
||||
# Supervisor interface |
||||
|
||||
@impl WebSocket |
||||
def child_spec(arg) do |
||||
Supervisor.child_spec(%{id: __MODULE__, start: {__MODULE__, :start_link, [arg]}}, []) |
||||
end |
||||
|
||||
@impl WebSocket |
||||
# only allow secure WSS |
||||
def start_link(["wss://" <> _ = url, gen_fsm_options]) when is_list(gen_fsm_options) do |
||||
fsm_name = |
||||
case Keyword.fetch(gen_fsm_options, :name) do |
||||
{:ok, name} when is_atom(name) -> {:local, name} |
||||
:error -> :undefined |
||||
end |
||||
|
||||
%URI{host: host} = URI.parse(url) |
||||
host_charlist = String.to_charlist(host) |
||||
|
||||
# `:depth`, `:verify`, and `:verify_fun`, are based on `:hackney_connect.ssl_opts_1/2` as we use `:hackney` through |
||||
# `:httpoison` and this keeps the SSL rules consistent between HTTP and WebSocket |
||||
:websocket_client.start_link(fsm_name, url, __MODULE__, url, |
||||
ssl_verify: :verify_peer, |
||||
socket_opts: [ |
||||
cacerts: :certifi.cacerts(), |
||||
depth: 99, |
||||
# SNI extension discloses host name in the clear, but allows for compatibility with Virtual Hosting for TLS |
||||
server_name_indication: host_charlist, |
||||
verify_fun: {&:ssl_verify_hostname.verify_fun/3, [check_hostname: host_charlist]} |
||||
] |
||||
) |
||||
end |
||||
|
||||
# Client interface |
||||
|
||||
@impl WebSocket |
||||
@spec json_rpc(WebSocket.web_socket(), Transport.request()) :: {:ok, Transport.result()} | {:error, reason :: term()} |
||||
def json_rpc(web_socket, request) do |
||||
GenServer.call(web_socket, {:json_rpc, request}) |
||||
end |
||||
|
||||
@impl WebSocket |
||||
@spec subscribe(WebSocket.web_socket(), Subscription.event(), Subscription.params()) :: |
||||
{:ok, Subscription.t()} | {:error, reason :: term()} |
||||
def subscribe(web_socket, event, params) when is_binary(event) and is_list(params) do |
||||
GenServer.call(web_socket, {:subscribe, event, params}) |
||||
end |
||||
|
||||
@impl WebSocket |
||||
@spec unsubscribe(WebSocket.web_socket(), Subscription.t()) :: :ok | {:error, :not_found} |
||||
def unsubscribe(web_socket, %Subscription{} = subscription) do |
||||
GenServer.call(web_socket, {:unsubscribe, subscription}) |
||||
end |
||||
|
||||
@impl :websocket_client |
||||
def init(url) do |
||||
{:reconnect, %__MODULE__{url: url}} |
||||
end |
||||
|
||||
@impl :websocket_client |
||||
def onconnect(_, %__MODULE__{} = state) do |
||||
{:ok, state} |
||||
end |
||||
|
||||
@impl :websocket_client |
||||
def ondisconnect(reason, %__MODULE__{} = state) do |
||||
{:close, reason, state} |
||||
end |
||||
|
||||
@impl :websocket_client |
||||
def websocket_handle({:text, text}, _request, %__MODULE__{} = state) do |
||||
case Jason.decode(text) do |
||||
{:ok, json} -> |
||||
handle_response(json, state) |
||||
|
||||
{:error, _} = error -> |
||||
broadcast(error, state) |
||||
{:ok, state} |
||||
end |
||||
end |
||||
|
||||
@impl :websocket_client |
||||
def websocket_info({:"$gen_call", from, request}, _, %__MODULE__{} = state) do |
||||
handle_call(request, from, state) |
||||
end |
||||
|
||||
@impl :websocket_client |
||||
def websocket_terminate(close, _request, %__MODULE__{} = state) do |
||||
broadcast(close, state) |
||||
end |
||||
|
||||
defp broadcast(message, %__MODULE__{subscription_id_to_subscription: id_to_subscription}) do |
||||
id_to_subscription |
||||
|> Map.values() |
||||
|> Subscription.broadcast(message) |
||||
end |
||||
|
||||
defp handle_call(message, from, %__MODULE__{} = state) do |
||||
{updated_state, unique_request} = register(message, from, state) |
||||
|
||||
{:reply, {:text, Jason.encode!(unique_request)}, updated_state} |
||||
end |
||||
|
||||
defp handle_response( |
||||
%{"method" => "eth_subscription", "params" => %{"result" => result, "subscription" => subscription_id}}, |
||||
%__MODULE__{subscription_id_to_subscription: subscription_id_to_subscription} = state |
||||
) do |
||||
case subscription_id_to_subscription do |
||||
%{^subscription_id => subscription} -> |
||||
Subscription.publish(subscription, {:ok, result}) |
||||
|
||||
_ -> |
||||
Logger.error(fn -> |
||||
[ |
||||
"Unexpected `eth_subscription` subscription ID (", |
||||
inspect(subscription_id), |
||||
") result (", |
||||
inspect(result), |
||||
"). Subscription ID not in known subscription IDs (", |
||||
subscription_id_to_subscription |
||||
|> Map.values() |
||||
|> Enum.map(&inspect/1), |
||||
")." |
||||
] |
||||
end) |
||||
end |
||||
|
||||
{:ok, state} |
||||
end |
||||
|
||||
defp handle_response( |
||||
%{"id" => id} = response, |
||||
%__MODULE__{request_id_to_registration: request_id_to_registration} = state |
||||
) do |
||||
{registration, new_request_id_to_registration} = Map.pop(request_id_to_registration, id) |
||||
respond_to_registration(registration, new_request_id_to_registration, response, state) |
||||
end |
||||
|
||||
defp handle_response(response, %__MODULE__{} = state) do |
||||
Logger.error(fn -> |
||||
[ |
||||
"Unexpected JSON response from web socket\n", |
||||
"\n", |
||||
" Response:\n", |
||||
" ", |
||||
inspect(response) |
||||
] |
||||
end) |
||||
|
||||
{:ok, state} |
||||
end |
||||
|
||||
defp register( |
||||
{:json_rpc, original_request}, |
||||
from, |
||||
%__MODULE__{request_id_to_registration: request_id_to_registration} = state |
||||
) do |
||||
unique_id = unique_request_id(state) |
||||
|
||||
{%__MODULE__{ |
||||
state |
||||
| request_id_to_registration: |
||||
Map.put(request_id_to_registration, unique_id, %Registration{ |
||||
from: from, |
||||
type: :json_rpc |
||||
}) |
||||
}, %{original_request | id: unique_id}} |
||||
end |
||||
|
||||
defp register( |
||||
{:subscribe, event, params}, |
||||
from, |
||||
%__MODULE__{request_id_to_registration: request_id_to_registration} = state |
||||
) |
||||
when is_binary(event) and is_list(params) do |
||||
unique_id = unique_request_id(state) |
||||
|
||||
{ |
||||
%__MODULE__{ |
||||
state |
||||
| request_id_to_registration: |
||||
Map.put(request_id_to_registration, unique_id, %Registration{from: from, type: :subscribe}) |
||||
}, |
||||
request(%{id: unique_id, method: "eth_subscribe", params: [event | params]}) |
||||
} |
||||
end |
||||
|
||||
defp register( |
||||
{:unsubscribe, %Subscription{id: subscription_id}}, |
||||
from, |
||||
%__MODULE__{request_id_to_registration: request_id_to_registration} = state |
||||
) do |
||||
unique_id = unique_request_id(state) |
||||
|
||||
{ |
||||
%__MODULE__{ |
||||
state |
||||
| request_id_to_registration: |
||||
Map.put(request_id_to_registration, unique_id, %Registration{ |
||||
from: from, |
||||
type: :unsubscribe, |
||||
subscription_id: subscription_id |
||||
}) |
||||
}, |
||||
request(%{id: unique_id, method: "eth_unsubscribe", params: [subscription_id]}) |
||||
} |
||||
end |
||||
|
||||
defp respond_to_registration( |
||||
%Registration{type: :json_rpc, from: from}, |
||||
new_request_id_to_registration, |
||||
response, |
||||
%__MODULE__{} = state |
||||
) do |
||||
reply = |
||||
case response do |
||||
%{"result" => result} -> {:ok, result} |
||||
%{"error" => error} -> {:error, error} |
||||
end |
||||
|
||||
GenServer.reply(from, reply) |
||||
|
||||
{:ok, %__MODULE__{state | request_id_to_registration: new_request_id_to_registration}} |
||||
end |
||||
|
||||
defp respond_to_registration( |
||||
%Registration{type: :subscribe, from: {subscriber_pid, _} = from}, |
||||
new_request_id_to_registration, |
||||
%{"result" => subscription_id}, |
||||
%__MODULE__{url: url} = state |
||||
) do |
||||
subscription = %Subscription{ |
||||
id: subscription_id, |
||||
subscriber_pid: subscriber_pid, |
||||
transport: EthereumJSONRPC.WebSocket, |
||||
transport_options: [web_socket: __MODULE__, web_socket_options: %{web_socket: self()}, url: url] |
||||
} |
||||
|
||||
GenServer.reply(from, {:ok, subscription}) |
||||
|
||||
new_state = |
||||
state |
||||
|> put_in([Access.key!(:request_id_to_registration)], new_request_id_to_registration) |
||||
|> put_in([Access.key!(:subscription_id_to_subscription), subscription_id], subscription) |
||||
|
||||
{:ok, new_state} |
||||
end |
||||
|
||||
defp respond_to_registration( |
||||
%Registration{type: :subscribe, from: from}, |
||||
new_request_id_to_registration, |
||||
%{"error" => error}, |
||||
%__MODULE__{} = state |
||||
) do |
||||
GenServer.reply(from, {:error, error}) |
||||
|
||||
{:ok, %__MODULE__{state | request_id_to_registration: new_request_id_to_registration}} |
||||
end |
||||
|
||||
defp respond_to_registration( |
||||
%Registration{type: :unsubscribe, from: from, subscription_id: subscription_id}, |
||||
new_request_id_to_registration, |
||||
response, |
||||
%__MODULE__{} = state |
||||
) do |
||||
reply = |
||||
case response do |
||||
%{"result" => true} -> :ok |
||||
%{"result" => false} -> {:error, :not_found} |
||||
%{"error" => %{"message" => "subscription not found"}} -> {:error, :not_found} |
||||
%{"error" => error} -> {:error, error} |
||||
end |
||||
|
||||
GenServer.reply(from, reply) |
||||
|
||||
new_state = |
||||
state |
||||
|> put_in([Access.key!(:request_id_to_registration)], new_request_id_to_registration) |
||||
|> update_in([Access.key!(:subscription_id_to_subscription)], &Map.delete(&1, subscription_id)) |
||||
|
||||
{:ok, new_state} |
||||
end |
||||
|
||||
defp respond_to_registration(nil, _, response, %__MODULE__{} = state) do |
||||
Logger.error(fn -> ["Got response for unregistered request ID: ", inspect(response)] end) |
||||
|
||||
{:ok, state} |
||||
end |
||||
|
||||
defp unique_request_id(%__MODULE__{request_id_to_registration: request_id_to_registration} = state) do |
||||
unique_request_id = EthereumJSONRPC.unique_request_id() |
||||
|
||||
case request_id_to_registration do |
||||
# collision |
||||
%{^unique_request_id => _} -> |
||||
unique_request_id(state) |
||||
|
||||
_ -> |
||||
unique_request_id |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,219 @@ |
||||
defmodule EthereumJSONRPC.WebSocketTest do |
||||
use EthereumJSONRPC.WebSocket.Case, async: true |
||||
|
||||
import EthereumJSONRPC, only: [request: 1] |
||||
import Mox |
||||
|
||||
alias EthereumJSONRPC.{Subscription, WebSocket} |
||||
|
||||
setup :verify_on_exit! |
||||
|
||||
describe "json_rpc/2" do |
||||
test "can get result", %{subscribe_named_arguments: subscribe_named_arguments} do |
||||
transport_options = subscribe_named_arguments[:transport_options] |
||||
|
||||
if transport_options[:web_socket] == EthereumJSONRPC.WebSocket.Mox do |
||||
expect(EthereumJSONRPC.WebSocket.Mox, :json_rpc, fn _, _ -> |
||||
{:ok, %{"number" => "0x0"}} |
||||
end) |
||||
end |
||||
|
||||
assert {:ok, %{"number" => "0x0"}} = |
||||
%{id: 1, method: "eth_getBlockByNumber", params: ["earliest", false]} |
||||
|> request() |
||||
|> WebSocket.json_rpc(transport_options) |
||||
end |
||||
|
||||
test "can get error", %{subscribe_named_arguments: subscribe_named_arguments} do |
||||
transport_options = subscribe_named_arguments[:transport_options] |
||||
|
||||
if transport_options[:web_socket] == EthereumJSONRPC.WebSocket.Mox do |
||||
expect(EthereumJSONRPC.WebSocket.Mox, :json_rpc, fn _, _ -> |
||||
{:error, |
||||
%{ |
||||
"code" => -32601, |
||||
"message" => "Method not found" |
||||
}} |
||||
end) |
||||
end |
||||
|
||||
# purposely misspell method to trigger error |
||||
assert {:error, |
||||
%{ |
||||
"code" => -32601, |
||||
# Message varies by variant, so don't match on it |
||||
"message" => _ |
||||
}} = |
||||
%{id: 1, method: "eth_getBlockByNumbe", params: ["earliest", false]} |
||||
|> request() |
||||
|> WebSocket.json_rpc(transport_options) |
||||
end |
||||
end |
||||
|
||||
describe "subscribe/2" do |
||||
test "can subscribe to newHeads", %{subscribe_named_arguments: subscribe_named_arguments} do |
||||
transport = Keyword.fetch!(subscribe_named_arguments, :transport) |
||||
transport_options = subscribe_named_arguments[:transport_options] |
||||
subscriber_pid = self() |
||||
|
||||
if transport_options[:web_socket] == EthereumJSONRPC.WebSocket.Mox do |
||||
expect(EthereumJSONRPC.WebSocket.Mox, :subscribe, fn _, _, _ -> |
||||
{:ok, |
||||
%Subscription{ |
||||
id: "0x1", |
||||
subscriber_pid: subscriber_pid, |
||||
transport: transport, |
||||
transport_options: transport_options |
||||
}} |
||||
end) |
||||
end |
||||
|
||||
assert {:ok, |
||||
%Subscription{ |
||||
id: subscription_id, |
||||
subscriber_pid: ^subscriber_pid, |
||||
transport: ^transport, |
||||
transport_options: ^transport_options |
||||
}} = WebSocket.subscribe("newHeads", [], transport_options) |
||||
|
||||
assert is_binary(subscription_id) |
||||
end |
||||
|
||||
test "delivers new heads to caller", %{ |
||||
block_interval: block_interval, |
||||
subscribe_named_arguments: subscribe_named_arguments |
||||
} do |
||||
transport_options = subscribe_named_arguments[:transport_options] |
||||
web_socket_module = Keyword.fetch!(transport_options, :web_socket) |
||||
subscriber_pid = self() |
||||
|
||||
if web_socket_module == EthereumJSONRPC.WebSocket.Mox do |
||||
expect(web_socket_module, :subscribe, fn _, _, _ -> |
||||
subscription = %Subscription{ |
||||
id: "0x1", |
||||
subscriber_pid: subscriber_pid, |
||||
transport: Keyword.fetch!(subscribe_named_arguments, :transport), |
||||
transport_options: transport_options |
||||
} |
||||
|
||||
Process.send_after(subscriber_pid, {subscription, {:ok, %{"number" => "0x1"}}}, block_interval) |
||||
|
||||
{:ok, subscription} |
||||
end) |
||||
end |
||||
|
||||
assert {:ok, subscription} = WebSocket.subscribe("newHeads", [], transport_options) |
||||
|
||||
assert_receive {^subscription, {:ok, %{"number" => _}}}, block_interval * 2 |
||||
end |
||||
end |
||||
|
||||
describe "unsubscribe/2" do |
||||
test "can unsubscribe", %{subscribe_named_arguments: subscribe_named_arguments} do |
||||
transport_options = subscribe_named_arguments[:transport_options] |
||||
web_socket_module = Keyword.fetch!(transport_options, :web_socket) |
||||
subscriber_pid = self() |
||||
|
||||
if web_socket_module == EthereumJSONRPC.WebSocket.Mox do |
||||
subscription = %Subscription{ |
||||
id: "0x1", |
||||
subscriber_pid: subscriber_pid, |
||||
transport: Keyword.fetch!(subscribe_named_arguments, :transport), |
||||
transport_options: transport_options |
||||
} |
||||
|
||||
web_socket_module |
||||
|> expect(:subscribe, fn _, _, _ -> {:ok, subscription} end) |
||||
|> expect(:unsubscribe, fn _, ^subscription -> :ok end) |
||||
end |
||||
|
||||
assert {:ok, subscription} = WebSocket.subscribe("newHeads", [], transport_options) |
||||
|
||||
assert :ok = WebSocket.unsubscribe(subscription) |
||||
end |
||||
|
||||
test "stops messages being sent to subscriber", %{ |
||||
block_interval: block_interval, |
||||
subscribe_named_arguments: subscribe_named_arguments |
||||
} do |
||||
transport_options = subscribe_named_arguments[:transport_options] |
||||
web_socket_module = Keyword.fetch!(transport_options, :web_socket) |
||||
subscriber_pid = self() |
||||
|
||||
if web_socket_module == EthereumJSONRPC.WebSocket.Mox do |
||||
subscription = %Subscription{ |
||||
id: "0x1", |
||||
subscriber_pid: subscriber_pid, |
||||
transport: Keyword.fetch!(subscribe_named_arguments, :transport), |
||||
transport_options: transport_options |
||||
} |
||||
|
||||
web_socket_module |
||||
|> expect(:subscribe, 2, fn pid, _, _ when is_pid(pid) -> |
||||
send(pid, {:subscribe, subscription}) |
||||
|
||||
{:ok, subscription} |
||||
end) |
||||
|> expect(:unsubscribe, fn pid, ^subscription when is_pid(pid) -> |
||||
send(pid, {:unsubscribe, subscription}) |
||||
|
||||
:ok |
||||
end) |
||||
end |
||||
|
||||
assert {:ok, first_subscription} = |
||||
WebSocket.subscribe("newHeads", [], subscribe_named_arguments[:transport_options]) |
||||
|
||||
assert {:ok, second_subscription} = |
||||
WebSocket.subscribe("newHeads", [], subscribe_named_arguments[:transport_options]) |
||||
|
||||
wait = block_interval * 2 |
||||
|
||||
assert_receive {^first_subscription, {:ok, %{"number" => _}}}, wait |
||||
assert_receive {^second_subscription, {:ok, %{"number" => _}}}, wait |
||||
|
||||
assert :ok = WebSocket.unsubscribe(first_subscription) |
||||
|
||||
clear_mailbox() |
||||
|
||||
# see the message on the second subscription, so that we don't have to wait for the refute_receive, which would |
||||
# wait the full timeout |
||||
assert_receive {^second_subscription, {:ok, %{"number" => _}}}, wait |
||||
refute_receive {^first_subscription, _} |
||||
end |
||||
|
||||
test "return error if already unsubscribed", %{subscribe_named_arguments: subscribe_named_arguments} do |
||||
transport_options = subscribe_named_arguments[:transport_options] |
||||
web_socket_module = Keyword.fetch!(transport_options, :web_socket) |
||||
subscriber_pid = self() |
||||
|
||||
if web_socket_module == EthereumJSONRPC.WebSocket.Mox do |
||||
subscription = %Subscription{ |
||||
id: "0x1", |
||||
subscriber_pid: subscriber_pid, |
||||
transport: Keyword.fetch!(subscribe_named_arguments, :transport), |
||||
transport_options: transport_options |
||||
} |
||||
|
||||
web_socket_module |
||||
|> expect(:subscribe, fn _, _, _ -> {:ok, subscription} end) |
||||
|> expect(:unsubscribe, fn _, ^subscription -> :ok end) |
||||
|> expect(:unsubscribe, fn _, ^subscription -> {:error, :not_found} end) |
||||
end |
||||
|
||||
assert {:ok, subscription} = WebSocket.subscribe("newHeads", [], transport_options) |
||||
assert :ok = WebSocket.unsubscribe(subscription) |
||||
|
||||
assert {:error, :not_found} = WebSocket.unsubscribe(subscription) |
||||
end |
||||
end |
||||
|
||||
defp clear_mailbox do |
||||
receive do |
||||
_ -> clear_mailbox() |
||||
after |
||||
0 -> |
||||
:ok |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,19 @@ |
||||
defmodule EthereumJSONRPC.Case.Geth.HTTPWebSocket do |
||||
@moduledoc """ |
||||
`EthereumJSONRPC.Case` for connecting to Geth using `EthereumJSONRPC.HTTP` for `json_rpc_named_arguments` |
||||
`transport` and `EthereumJSONRPC.WebSocket` for `subscribe_named_arguments` `transport`. |
||||
""" |
||||
|
||||
def setup do |
||||
EthereumJSONRPC.WebSocket.Case.Geth.setup() |
||||
|> Map.put(:json_rpc_named_arguments, |
||||
transport: EthereumJSONRPC.HTTP, |
||||
transport_options: [ |
||||
http: EthereumJSONRPC.HTTP.HTTPoison, |
||||
http_options: [recv_timeout: 60_000, timeout: 60_000, hackney: [pool: :ethereum_jsonrpc]], |
||||
url: "https://mainnet.infura.io/8lTvJTKmHPCHazkneJsY" |
||||
], |
||||
variant: EthereumJSONRPC.Geth |
||||
) |
||||
end |
||||
end |
@ -0,0 +1,13 @@ |
||||
defmodule EthereumJSONRPC.Case.Geth.Mox do |
||||
@moduledoc """ |
||||
`EthereumJSONRPC.Case` for mocking connecting to Geth using `Mox` |
||||
""" |
||||
|
||||
def setup do |
||||
%{ |
||||
block_interval: 500, |
||||
json_rpc_named_arguments: [transport: EthereumJSONRPC.Mox, transport_options: [], variant: EthereumJSONRPC.Geth], |
||||
subscribe_named_arguments: [transport: EthereumJSONRPC.Mox, transport_options: []] |
||||
} |
||||
end |
||||
end |
@ -0,0 +1,20 @@ |
||||
defmodule EthereumJSONRPC.Case.Parity.HTTPWebSocket do |
||||
@moduledoc """ |
||||
`EthereumJSONRPC.Case` for connecting to Parity using `EthereumJSONRPC.HTTP` for `json_rpc_named_arguments` |
||||
`transport` and `EthereumJSONRPC.WebSocket` for `subscribe_named_arguments` `transport`. |
||||
""" |
||||
|
||||
def setup do |
||||
EthereumJSONRPC.WebSocket.Case.Parity.setup() |
||||
|> Map.put( |
||||
:json_rpc_named_arguments, |
||||
transport: EthereumJSONRPC.HTTP, |
||||
transport_options: [ |
||||
http: EthereumJSONRPC.HTTP.HTTPoison, |
||||
http_options: [recv_timeout: 60_000, timeout: 60_000, hackney: [pool: :ethereum_jsonrpc]], |
||||
url: "https://sokol-trace.poa.network" |
||||
], |
||||
variant: EthereumJSONRPC.Parity |
||||
) |
||||
end |
||||
end |
@ -0,0 +1,13 @@ |
||||
defmodule EthereumJSONRPC.Case.Parity.Mox do |
||||
@moduledoc """ |
||||
`EthereumJSONRPC.Case` for mocking connecting to Parity using `Mox` |
||||
""" |
||||
|
||||
def setup do |
||||
%{ |
||||
block_interval: 500, |
||||
json_rpc_named_arguments: [transport: EthereumJSONRPC.Mox, transport_options: [], variant: EthereumJSONRPC.Parity], |
||||
subscribe_named_arguments: [transport: EthereumJSONRPC.Mox, transport_options: []] |
||||
} |
||||
end |
||||
end |
@ -0,0 +1,9 @@ |
||||
defmodule EthereumJSONRPC.WebSocket.Case do |
||||
use ExUnit.CaseTemplate |
||||
|
||||
import EthereumJSONRPC.Case, only: [module: 2] |
||||
|
||||
setup do |
||||
module("ETHEREUM_JSONRPC_WEB_SOCKET_CASE", "EthereumJSONRPC.WebSocket.Case.Mox").setup() |
||||
end |
||||
end |
@ -0,0 +1,25 @@ |
||||
defmodule EthereumJSONRPC.WebSocket.Case.Geth do |
||||
@moduledoc """ |
||||
`EthereumJSONRPC.WebSocket.Case` connecting to Geth. |
||||
""" |
||||
|
||||
import ExUnit.Callbacks, only: [start_supervised!: 1] |
||||
|
||||
def setup do |
||||
url = "wss://mainnet.infura.io/ws/8lTvJTKmHPCHazkneJsY" |
||||
web_socket_module = EthereumJSONRPC.WebSocket.WebSocketClient |
||||
web_socket = start_supervised!({web_socket_module, [url, []]}) |
||||
|
||||
%{ |
||||
block_interval: 25_000, |
||||
subscribe_named_arguments: [ |
||||
transport: EthereumJSONRPC.WebSocket, |
||||
transport_options: [ |
||||
web_socket: web_socket_module, |
||||
web_socket_options: %{web_socket: web_socket}, |
||||
url: url |
||||
] |
||||
] |
||||
} |
||||
end |
||||
end |
@ -0,0 +1,76 @@ |
||||
defmodule EthereumJSONRPC.WebSocket.Case.Mox do |
||||
@moduledoc """ |
||||
`EthereumJSONRPC.WebSocket.Case` using `Mox` |
||||
""" |
||||
|
||||
import ExUnit.Callbacks, only: [start_supervised!: 1] |
||||
import Mox |
||||
|
||||
alias EthereumJSONRPC.Subscription |
||||
|
||||
@block_interval 250 |
||||
|
||||
def setup do |
||||
web_socket_module = EthereumJSONRPC.WebSocket.Mox |
||||
|
||||
web_socket_module |
||||
|> allow(self(), supervisor()) |
||||
|> stub(:child_spec, fn arguments -> |
||||
Supervisor.child_spec( |
||||
%{ |
||||
id: web_socket_module, |
||||
start: {web_socket_module, :start_link, arguments} |
||||
}, |
||||
[] |
||||
) |
||||
end) |
||||
|> stub(:start_link, fn _ -> |
||||
Task.start_link(__MODULE__, :loop, [%{}]) |
||||
end) |
||||
|
||||
url = "wss://example.com/ws" |
||||
web_socket = start_supervised!({web_socket_module, [url]}) |
||||
|
||||
%{ |
||||
block_interval: @block_interval, |
||||
subscribe_named_arguments: [ |
||||
transport: EthereumJSONRPC.WebSocket, |
||||
transport_options: [ |
||||
web_socket: web_socket_module, |
||||
web_socket_options: %{web_socket: web_socket}, |
||||
url: url |
||||
] |
||||
] |
||||
} |
||||
end |
||||
|
||||
def loop(%{subscription: subscription, timer_reference: timer_reference}) do |
||||
receive do |
||||
{:unsubscribe, ^subscription} -> |
||||
{:ok, :cancel} = :timer.cancel(timer_reference) |
||||
loop(%{}) |
||||
end |
||||
end |
||||
|
||||
def loop(%{}) do |
||||
receive do |
||||
{:subscribe, %Subscription{subscriber_pid: subscriber_pid} = subscription} -> |
||||
{:ok, timer_reference} = |
||||
:timer.send_interval(@block_interval, subscriber_pid, {subscription, {:ok, %{"number" => "0x1"}}}) |
||||
|
||||
loop(%{subscription: subscription, timer_reference: timer_reference}) |
||||
end |
||||
end |
||||
|
||||
defp supervisor do |
||||
case ExUnit.OnExitHandler.get_supervisor(self()) do |
||||
{:ok, nil} -> |
||||
{:ok, sup} = Supervisor.start_link([], strategy: :one_for_one, max_restarts: 1_000_000, max_seconds: 1) |
||||
ExUnit.OnExitHandler.put_supervisor(self(), sup) |
||||
sup |
||||
|
||||
{:ok, sup} -> |
||||
sup |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,25 @@ |
||||
defmodule EthereumJSONRPC.WebSocket.Case.Parity do |
||||
@moduledoc """ |
||||
`EthereumJSONRPC.WebSocket.Case` connecting to Parity. |
||||
""" |
||||
|
||||
import ExUnit.Callbacks, only: [start_supervised!: 1] |
||||
|
||||
def setup do |
||||
url = "wss://sokol-ws.poa.network/ws" |
||||
web_socket_module = EthereumJSONRPC.WebSocket.WebSocketClient |
||||
web_socket = start_supervised!({web_socket_module, [url, []]}) |
||||
|
||||
%{ |
||||
block_interval: 5_000, |
||||
subscribe_named_arguments: [ |
||||
transport: EthereumJSONRPC.WebSocket, |
||||
transport_options: [ |
||||
web_socket: web_socket_module, |
||||
web_socket_options: %{web_socket: web_socket}, |
||||
url: url |
||||
] |
||||
] |
||||
} |
||||
end |
||||
end |
@ -0,0 +1,110 @@ |
||||
defmodule Indexer.BlockFetcher.Catchup.Supervisor do |
||||
@moduledoc """ |
||||
Supervises the `Indexer.BlockerFetcher.Catchup` with exponential backoff for restarts. |
||||
""" |
||||
|
||||
# NOT a `Supervisor` because of the `Task` restart strategies are custom. |
||||
use GenServer |
||||
|
||||
require Logger |
||||
|
||||
alias Indexer.{BlockFetcher, BoundInterval} |
||||
alias Indexer.BlockFetcher.Catchup |
||||
|
||||
# milliseconds |
||||
@block_interval 5_000 |
||||
|
||||
@enforce_keys ~w(bound_interval catchup)a |
||||
defstruct bound_interval: nil, |
||||
catchup: %Catchup{}, |
||||
task: nil |
||||
|
||||
def child_spec(arg) do |
||||
# The `child_spec` from `use Supervisor` because the one from `use GenServer` will set the `type` to `:worker` |
||||
# instead of `:supervisor` and use the wrong shutdown timeout |
||||
Supervisor.child_spec(%{id: __MODULE__, start: {__MODULE__, :start_link, [arg]}, type: :supervisor}, []) |
||||
end |
||||
|
||||
@doc """ |
||||
Starts supervisor of `Indexer.BlockerFetcher.Catchup` and `Indexer.BlockFetcher.Realtime`. |
||||
|
||||
For `named_arguments` see `Indexer.BlockFetcher.new/1`. For `t:GenServer.options/0` see `GenServer.start_link/3`. |
||||
""" |
||||
@spec start_link([named_arguments :: list() | GenServer.options()]) :: {:ok, pid} |
||||
def start_link([named_arguments, gen_server_options]) when is_map(named_arguments) and is_list(gen_server_options) do |
||||
GenServer.start_link(__MODULE__, named_arguments, gen_server_options) |
||||
end |
||||
|
||||
@impl GenServer |
||||
def init(named_arguments) do |
||||
state = new(named_arguments) |
||||
|
||||
send(self(), :catchup_index) |
||||
|
||||
{:ok, state} |
||||
end |
||||
|
||||
defp new(%{block_fetcher: common_block_fetcher} = named_arguments) do |
||||
block_fetcher = %BlockFetcher{common_block_fetcher | broadcast: false, callback_module: Catchup} |
||||
|
||||
block_interval = Map.get(named_arguments, :block_interval, @block_interval) |
||||
minimum_interval = div(block_interval, 2) |
||||
bound_interval = BoundInterval.within(minimum_interval..(minimum_interval * 10)) |
||||
|
||||
%__MODULE__{ |
||||
catchup: %Catchup{block_fetcher: block_fetcher}, |
||||
bound_interval: bound_interval |
||||
} |
||||
end |
||||
|
||||
@impl GenServer |
||||
def handle_info(:catchup_index, %__MODULE__{catchup: %Catchup{} = catchup} = state) do |
||||
{:noreply, |
||||
%__MODULE__{state | task: Task.Supervisor.async_nolink(Indexer.TaskSupervisor, Catchup, :task, [catchup])}} |
||||
end |
||||
|
||||
def handle_info( |
||||
{ref, %{first_block_number: first_block_number, missing_block_count: missing_block_count}}, |
||||
%__MODULE__{ |
||||
bound_interval: bound_interval, |
||||
task: %Task{ref: ref} |
||||
} = state |
||||
) |
||||
when is_integer(missing_block_count) do |
||||
new_bound_interval = |
||||
case missing_block_count do |
||||
0 -> |
||||
Logger.info("Index already caught up in #{first_block_number}-0") |
||||
|
||||
BoundInterval.increase(bound_interval) |
||||
|
||||
_ -> |
||||
Logger.info("Index had to catch up #{missing_block_count} blocks in #{first_block_number}-0") |
||||
|
||||
BoundInterval.decrease(bound_interval) |
||||
end |
||||
|
||||
Process.demonitor(ref, [:flush]) |
||||
|
||||
interval = new_bound_interval.current |
||||
|
||||
Logger.info(fn -> |
||||
"Checking if index needs to catch up in #{interval}ms" |
||||
end) |
||||
|
||||
Process.send_after(self(), :catchup_index, interval) |
||||
|
||||
{:noreply, %__MODULE__{state | bound_interval: new_bound_interval, task: nil}} |
||||
end |
||||
|
||||
def handle_info( |
||||
{:DOWN, ref, :process, pid, reason}, |
||||
%__MODULE__{task: %Task{pid: pid, ref: ref}} = state |
||||
) do |
||||
Logger.error(fn -> "Catchup index stream exited with reason (#{inspect(reason)}). Restarting" end) |
||||
|
||||
send(self(), :catchup_index) |
||||
|
||||
{:noreply, %__MODULE__{state | task: nil}} |
||||
end |
||||
end |
@ -0,0 +1,37 @@ |
||||
defmodule Indexer.BlockFetcher.Realtime.Supervisor do |
||||
@moduledoc """ |
||||
Supervises realtime block fetcher. |
||||
""" |
||||
|
||||
use Supervisor |
||||
|
||||
def start_link([arguments, gen_server_options]) do |
||||
Supervisor.start_link(__MODULE__, arguments, gen_server_options) |
||||
end |
||||
|
||||
@impl Supervisor |
||||
def init(%{block_fetcher: block_fetcher, subscribe_named_arguments: subscribe_named_arguments}) do |
||||
children = |
||||
case Keyword.fetch!(subscribe_named_arguments, :transport) do |
||||
EthereumJSONRPC.WebSocket -> |
||||
transport_options = Keyword.fetch!(subscribe_named_arguments, :transport_options) |
||||
url = Keyword.fetch!(transport_options, :url) |
||||
web_socket_module = Keyword.fetch!(transport_options, :web_socket) |
||||
web_socket = Indexer.BlockFetcher.Realtime.WebSocket |
||||
|
||||
block_fetcher_subscribe_named_arguments = |
||||
put_in(subscribe_named_arguments[:transport_options][:web_socket_options], %{web_socket: web_socket}) |
||||
|
||||
[ |
||||
{web_socket_module, [url, [name: web_socket]]}, |
||||
{Indexer.BlockFetcher.Realtime, |
||||
[ |
||||
%{block_fetcher: block_fetcher, subscribe_named_arguments: block_fetcher_subscribe_named_arguments}, |
||||
[name: Indexer.BlockFetcher.Realtime] |
||||
]} |
||||
] |
||||
end |
||||
|
||||
Supervisor.init(children, strategy: :rest_for_one) |
||||
end |
||||
end |
@ -1,84 +1,35 @@ |
||||
defmodule Indexer.BlockFetcher.Supervisor do |
||||
@moduledoc """ |
||||
Supervises the `Indexer.BlockerFetcher.Catchup` and `Indexer.BlockFetcher.Realtime`. |
||||
Supervises catchup and realtime block fetchers |
||||
""" |
||||
|
||||
# NOT a `Supervisor` because of the `Task` restart strategies are custom. |
||||
use GenServer |
||||
|
||||
require Logger |
||||
|
||||
alias Indexer.BlockFetcher |
||||
alias Indexer.BlockFetcher.{Catchup, Realtime} |
||||
|
||||
# milliseconds |
||||
@block_interval 5_000 |
||||
|
||||
@enforce_keys ~w(catchup realtime)a |
||||
defstruct ~w(catchup realtime)a |
||||
|
||||
def child_spec(arg) do |
||||
# The `child_spec` from `use Supervisor` because the one from `use GenServer` will set the `type` to `:worker` |
||||
# instead of `:supervisor` and use the wrong shutdown timeout |
||||
Supervisor.child_spec(%{id: __MODULE__, start: {__MODULE__, :start_link, [arg]}, type: :supervisor}, []) |
||||
end |
||||
|
||||
@doc """ |
||||
Starts supervisor of `Indexer.BlockerFetcher.Catchup` and `Indexer.BlockFetcher.Realtime`. |
||||
|
||||
For `named_arguments` see `Indexer.BlockFetcher.new/1`. For `t:GenServer.options/0` see `GenServer.start_link/3`. |
||||
""" |
||||
@spec start_link([named_arguments :: list() | GenServer.options()]) :: {:ok, pid} |
||||
def start_link([named_arguments, gen_server_options]) when is_list(named_arguments) and is_list(gen_server_options) do |
||||
GenServer.start_link(__MODULE__, named_arguments, gen_server_options) |
||||
end |
||||
|
||||
@impl GenServer |
||||
def init(named_arguments) do |
||||
state = new(named_arguments) |
||||
|
||||
send(self(), :catchup_index) |
||||
{:ok, _} = :timer.send_interval(state.realtime.interval, :realtime_index) |
||||
|
||||
{:ok, state} |
||||
end |
||||
|
||||
defp new(named_arguments) do |
||||
{given_block_interval, block_fetcher_named_arguments} = Keyword.pop(named_arguments, :block_interval) |
||||
block_fetcher = struct!(BlockFetcher, block_fetcher_named_arguments) |
||||
block_interval = given_block_interval || @block_interval |
||||
|
||||
%__MODULE__{ |
||||
catchup: Catchup.new(%{block_fetcher: block_fetcher, block_interval: block_interval}), |
||||
realtime: Realtime.new(%{block_fetcher: block_fetcher, block_interval: block_interval}) |
||||
} |
||||
end |
||||
|
||||
@impl GenServer |
||||
def handle_info(:catchup_index, %__MODULE__{} = state) do |
||||
{:noreply, Catchup.put(state)} |
||||
end |
||||
|
||||
def handle_info({ref, _} = message, %__MODULE__{catchup: %Catchup{task: %Task{ref: ref}}} = state) do |
||||
{:noreply, Catchup.handle_success(message, state)} |
||||
end |
||||
|
||||
def handle_info( |
||||
{:DOWN, ref, :process, pid, _} = message, |
||||
%__MODULE__{catchup: %Catchup{task: %Task{pid: pid, ref: ref}}} = state |
||||
) do |
||||
{:noreply, Catchup.handle_failure(message, state)} |
||||
end |
||||
|
||||
def handle_info(:realtime_index, %__MODULE__{} = state) do |
||||
{:noreply, Realtime.put(state)} |
||||
end |
||||
|
||||
def handle_info({ref, :ok} = message, %__MODULE__{} = state) when is_reference(ref) do |
||||
{:noreply, Realtime.handle_success(message, state)} |
||||
end |
||||
|
||||
def handle_info({:DOWN, _, :process, _, _} = message, %__MODULE__{} = state) do |
||||
{:noreply, Realtime.handle_failure(message, state)} |
||||
use Supervisor |
||||
|
||||
def start_link([arguments, gen_server_options]) do |
||||
Supervisor.start_link(__MODULE__, arguments, gen_server_options) |
||||
end |
||||
|
||||
@impl Supervisor |
||||
def init(%{block_interval: block_interval, subscribe_named_arguments: subscribe_named_arguments} = named_arguments) do |
||||
block_fetcher = |
||||
named_arguments |
||||
|> Map.drop(~w(block_interval subscribe_named_arguments)a) |
||||
|> BlockFetcher.new() |
||||
|
||||
Supervisor.init( |
||||
[ |
||||
{Catchup.Supervisor, |
||||
[%{block_fetcher: block_fetcher, block_interval: block_interval}, [name: Catchup.Supervisor]]}, |
||||
{Realtime.Supervisor, |
||||
[ |
||||
%{block_fetcher: block_fetcher, subscribe_named_arguments: subscribe_named_arguments}, |
||||
[name: Realtime.Supervisor] |
||||
]} |
||||
], |
||||
strategy: :one_for_one |
||||
) |
||||
end |
||||
end |
||||
|
Loading…
Reference in new issue