Current section

Files

Jump to
snakepit lib snakepit grpc client.ex
Raw

lib/snakepit/grpc/client.ex

defmodule Snakepit.GRPC.Client do
@moduledoc """
gRPC client wrapper for Snakepit bridge communication.
"""
require Logger
alias Snakepit.Grpc.{
SnakepitBridge.Stub,
ExecuteRequest,
ExecuteResponse,
SessionRequest,
HealthRequest,
HealthResponse,
InfoRequest,
InfoResponse
}
@doc """
Connect to a gRPC server at the given host and port.
"""
def connect(host, port) do
GRPC.Stub.connect("#{host}:#{port}")
end
@doc """
Execute a command via gRPC.
"""
def execute(channel, command, args, timeout \\ 30_000) do
request = %ExecuteRequest{
command: command,
args: encode_args(args),
timeout_ms: timeout,
request_id: generate_request_id()
}
case Stub.execute(channel, request, timeout: timeout) do
{:ok, %ExecuteResponse{success: true} = response} ->
{:ok, decode_result(response.result)}
{:ok, %ExecuteResponse{success: false, error: error}} ->
{:error, error}
{:error, reason} ->
{:error, reason}
end
end
@doc """
Execute a streaming command with a callback function.
"""
def execute_stream(channel, command, args, callback_fn, timeout \\ 300_000) do
request_id = generate_request_id()
Logger.info("[GRPC.Client] execute_stream - command: #{command}, request_id: #{request_id}")
request = %ExecuteRequest{
command: command,
args: encode_args(args),
timeout_ms: timeout,
request_id: request_id
}
Logger.info("[GRPC.Client] Calling Stub.execute_stream with timeout: #{timeout}")
# Call the generated stub function and consume the stream with callback
case Stub.execute_stream(channel, request, timeout: timeout) do
{:ok, stream} ->
Logger.info("[GRPC.Client] Got stream from stub, consuming with callback")
# Consume the gRPC stream properly
stream
|> Enum.each(fn response ->
case response do
{:ok, stream_response} ->
# Decode the chunk and call the callback
decoded_chunk = decode_result(stream_response.chunk)
callback_fn.(%{
"data" => decoded_chunk,
"is_final" => stream_response.is_final,
"error" => stream_response.error
})
{:error, reason} ->
callback_fn.(%{"error" => inspect(reason), "is_final" => true})
end
end)
:ok
{:error, reason} ->
Logger.error("[GRPC.Client] Stub returned error: #{inspect(reason)}")
{:error, reason}
end
rescue
e ->
Logger.error("[GRPC.Client] Exception in execute_stream: #{inspect(e)}")
{:error, e}
end
@doc """
Execute a command within a session.
"""
def execute_in_session(channel, session_id, command, args, timeout \\ 30_000) do
request = %SessionRequest{
session_id: session_id,
command: command,
args: encode_args(args),
timeout_ms: timeout,
request_id: generate_request_id()
}
case Stub.execute_in_session(channel, request, timeout: timeout) do
{:ok, %ExecuteResponse{success: true} = response} ->
{:ok, decode_result(response.result)}
{:ok, %ExecuteResponse{success: false, error: error}} ->
{:error, error}
{:error, reason} ->
{:error, reason}
end
end
@doc """
Initiate a session-based streaming command and return the stream.
"""
def execute_in_session_stream(channel, session_id, command, args, timeout \\ 300_000) do
request = %SessionRequest{
session_id: session_id,
command: command,
args: encode_args(args),
timeout_ms: timeout,
request_id: generate_request_id()
}
# Return the result from the gRPC library directly
Stub.execute_in_session_stream(channel, request, timeout: timeout)
rescue
e ->
{:error, e}
end
@doc """
Check health of the gRPC server.
"""
def health(channel, worker_id \\ "") do
request = %HealthRequest{worker_id: worker_id}
case Stub.health(channel, request, timeout: 5_000) do
{:ok, %HealthResponse{} = response} ->
{:ok, response}
{:error, reason} ->
{:error, reason}
end
end
@doc """
Get information about the worker.
"""
def get_info(channel, include_capabilities \\ true, include_stats \\ true) do
request = %InfoRequest{
include_capabilities: include_capabilities,
include_stats: include_stats
}
case Stub.get_info(channel, request, timeout: 10_000) do
{:ok, %InfoResponse{} = response} ->
{:ok, response}
{:error, reason} ->
{:error, reason}
end
end
# Private helper functions
defp encode_args(args) when is_map(args) do
Map.new(args, fn {k, v} ->
{to_string(k), encode_value(v)}
end)
end
defp encode_value(v) when is_binary(v), do: v
defp encode_value(v), do: Jason.encode!(v)
defp decode_result(result) when is_map(result) do
Map.new(result, fn {k, v} ->
{k, decode_value(v)}
end)
end
defp decode_value(v) do
case Jason.decode(v) do
{:ok, decoded} -> decoded
# Keep as binary if not JSON
_ -> v
end
end
defp generate_request_id do
:crypto.strong_rand_bytes(8) |> Base.encode16(case: :lower)
end
end