Current section
Files
Jump to
Current section
Files
lib/connection/executor/request_executor.ex
defmodule Ravix.Connection.RequestExecutor do
@moduledoc false
use Retry
require OK
require Logger
alias Ravix.Connection.State, as: ConnectionState
alias Ravix.Connection.{ServerNode, NodeSelector, Response}
alias Ravix.Connection.RequestExecutor
@doc """
Executes a RavenDB command for the informed connection
## Parameters
- command: The command that will be executed, must be a RavenCommand
- conn_state: The connection state for which this execution will be linked
- headers: HTTP headers to send to RavenDB
- opts: Request options
## Returns
- `{:ok, Ravix.Connection.Response}` for a successful synchronous call
- `{:ok, Enumerable}` if it's a streammed command
- `{:error, cause}` if the request fails
"""
@spec execute(struct(), ConnectionState.t(), tuple(), keyword()) ::
{:ok, Response.t()} | {:ok, Enumerable.t()} | {:error, any()}
def execute(
command,
%ConnectionState{} = conn_state,
headers \\ {},
opts \\ []
) do
pool_name = NodeSelector.current_node(conn_state)
opts = opts ++ RequestExecutor.Options.from_connection_state(conn_state)
headers =
case conn_state.disable_topology_updates do
true -> headers
false -> [{"Topology-Etag", conn_state.topology_etag}]
end
execute_with_node_pool(command, pool_name, headers, opts)
end
@doc """
Executes a RavenDB command on the informed node
## Parameters
- command: The command that will be executed, must be a RavenCommand
- pid: The PID of the node
- headers: HTTP headers to send to RavenDB
- opts: Request options
## Returns
- `{:ok, Ravix.Connection.Response}` for a successful synchronous call
- `{:ok, Enumerable}` if it's a streammed command
- `{:error, cause}` if the request fails
"""
@spec execute_with_node(struct(), pid(), tuple(), keyword()) ::
{:ok, Response.t()} | {:ok, Enumerable.t()} | {:error, any()}
def execute_with_node(command, pid, headers \\ {}, opts \\ []) do
call_raven(pid, command, headers, opts)
end
@doc """
Executes a RavenDB command in any node of the informed pool
## Parameters
- command: The command that will be executed, must be a RavenCommand
- pool_name: The Pool name
- headers: HTTP headers to send to RavenDB
- opts: Request options
## Returns
- `{:ok, Ravix.Connection.Response}` for a successful synchronous call
- `{:ok, Enumerable}` if it's a streammed command
- `{:error, cause}` if the request fails
"""
@spec execute_with_node_pool(struct(), binary(), tuple(), keyword()) ::
{:ok, Response.t()} | {:ok, Enumerable.t()} | {:error, any()}
def execute_with_node_pool(command, pool_name, headers, opts) do
:poolboy.transaction(
pool_id(pool_name),
fn pid ->
call_raven(pid, command, headers, opts)
end
)
end
@spec call_raven(pid(), struct(), tuple(), keyword()) ::
{:ok, Response.t()} | {:ok, Enumerable.t()} | {:error, any()}
defp call_raven(pid, %{is_stream: false} = command, headers, opts) do
should_retry = Keyword.get(opts, :retry_on_failure, false)
retry_backoff = Keyword.get(opts, :retry_backoff, 100)
timeout = Keyword.get(opts, :timeout, 15000)
retry_count =
case should_retry do
true -> Keyword.get(opts, :retry_count, 3)
false -> 0
end
retry with: constant_backoff(retry_backoff) |> Stream.take(retry_count) do
GenServer.call(pid, {:request, command, headers, opts}, timeout)
after
{:ok, result} -> {:ok, result}
{:non_retryable_error, response} -> {:error, response}
{:error, error_response} -> {:error, error_response}
else
err -> err
end
end
defp call_raven(pid, command, headers, opts) do
init_request = fn ->
GenServer.call(pid, {:request, command, headers, opts})
end
read_buffer = fn {:ok, request_ref} ->
case GenServer.call(pid, {:read_request_buffer, request_ref}) do
{:ok, :halt} -> {:halt, {:ok, request_ref}}
{:ok, chunk} -> {[chunk], {:ok, request_ref}}
end
end
no_op = fn _request_ref ->
nil
end
{:ok,
Stream.resource(init_request, read_buffer, no_op)
|> Jaxon.Stream.from_enumerable()
|> Jaxon.Stream.query([:root, "Results", :all])}
end
@doc """
Fetches the current node executor state
## Parameters
pid = The PID of the node
## Returns
- `{:ok, Ravix.Connection.ServerNode}` if there's a node
- `{:error, :node_not_found}` if there's not a node with the informed pid
"""
@spec fetch_node_state(pid) :: {:ok, ServerNode.t()} | {:error, :node_not_found}
def fetch_node_state(pid) when is_pid(pid) do
try do
{:ok, pid |> :sys.get_state()}
catch
:exit, _ -> {:error, :node_not_found}
end
end
@spec start_link(any, ServerNode.t()) :: :ignore | {:error, any} | {:ok, pid}
def start_link(_attrs, %ServerNode{} = node) do
poolboy_config = [
{:name, pool_registry(node)},
{:worker_module, Ravix.Connection.RequestExecutor.Worker},
{:size, node.min_pool_size},
{:max_overflow, node.max_pool_size}
]
executor_config =
Keyword.new(Map.from_struct(node), fn {k, v} ->
{k, v}
end)
:poolboy.start_link(
poolboy_config,
executor_config
)
end
@spec child_spec(ServerNode.t()) :: %{
id: atom,
start: {RequestExecutor, :start_link, [map, ...]}
}
def child_spec(%ServerNode{} = node) do
%{
id: String.to_atom(node.url <> "_" <> node.database),
start: {__MODULE__, :start_link, [node]}
}
end
defp pool_registry(%ServerNode{} = node) do
{:via, Registry, {:request_executor_pools, NodeSelector.node_id(node), node}}
end
defp pool_id(pool_id) do
{:via, Registry, {:request_executor_pools, pool_id}}
end
end