Packages
snakepit
0.4.0
0.13.0
0.12.0
0.11.1
0.11.0
0.10.1
0.10.0
0.9.1
0.9.0
0.8.9
0.8.8
0.8.7
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.11
0.6.10
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.1
0.5.0
0.4.3
0.4.2
0.4.1
0.4.0
0.3.3
0.3.2
0.3.1
0.3.0
0.2.1
0.2.0
0.1.2
0.1.1
0.1.0
High-performance pooler and session manager for external language integrations. Supports Python, Node.js, Ruby, and more with gRPC streaming, session management, and production-ready process cleanup.
Current section
Files
Jump to
Current section
Files
lib/snakepit/grpc/client_impl.ex
defmodule Snakepit.GRPC.ClientImpl do
@moduledoc """
Real gRPC client implementation using generated stubs.
"""
require Logger
alias Snakepit.Bridge
alias Snakepit.Bridge.Serialization
@default_timeout 30_000
def connect(port) when is_integer(port) do
connect("localhost:#{port}")
end
def connect(address) when is_binary(address) do
opts = []
case GRPC.Stub.connect(address, opts) do
{:ok, channel} ->
# Verify connection with ping
case ping(channel, "connection_test") do
{:ok, _} -> {:ok, channel}
error -> error
end
error ->
error
end
end
def ping(channel, message, opts \\ []) do
request = %Bridge.PingRequest{message: message}
timeout = opts[:timeout] || @default_timeout
call_opts = [timeout: timeout]
case Bridge.BridgeService.Stub.ping(channel, request, call_opts) do
{:ok, response, _headers} ->
{:ok,
%{
message: response.message,
server_time: response.server_time
}}
error ->
handle_error(error)
end
end
def initialize_session(channel, session_id, config \\ %{}, opts \\ []) do
metadata = %{
"elixir_node" => to_string(node()),
"initialized_at" => DateTime.to_iso8601(DateTime.utc_now())
}
session_config = %Bridge.SessionConfig{
enable_caching: Map.get(config, :enable_caching, true),
cache_ttl_seconds: Map.get(config, :cache_ttl_seconds, 60),
enable_telemetry: Map.get(config, :enable_telemetry, false)
}
request = %Bridge.InitializeSessionRequest{
session_id: session_id,
metadata: metadata,
config: session_config
}
timeout = opts[:timeout] || @default_timeout
call_opts = [timeout: timeout]
case Bridge.BridgeService.Stub.initialize_session(channel, request, call_opts) do
{:ok, response, _headers} ->
{:ok,
%{
success: response.success,
available_tools: response.available_tools,
initial_variables: response.initial_variables,
error_message: response.error_message
}}
error ->
handle_error(error)
end
end
def cleanup_session(channel, session_id, force \\ false, opts \\ []) do
request = %Bridge.CleanupSessionRequest{
session_id: session_id,
force: force
}
timeout = opts[:timeout] || @default_timeout
call_opts = [timeout: timeout]
case Bridge.BridgeService.Stub.cleanup_session(channel, request, call_opts) do
{:ok, response, _headers} ->
{:ok,
%{
success: response.success,
resources_cleaned: response.resources_cleaned
}}
error ->
handle_error(error)
end
end
def register_variable(channel, session_id, name, type, initial_value, opts \\ []) do
require Logger
# Encode value
case Serialization.encode_any(initial_value, type) do
{:ok, any_value, _binary_data} ->
# Convert to protobuf Any
proto_any = %Google.Protobuf.Any{
type_url: any_value.type_url,
value: any_value.value
}
# Encode constraints if provided
constraints_json =
case opts[:constraints] do
nil -> ""
constraints -> Jason.encode!(constraints)
end
request = %Bridge.RegisterVariableRequest{
session_id: session_id,
name: to_string(name),
type: to_string(type),
initial_value: proto_any,
constraints_json: constraints_json,
metadata: opts[:metadata] || %{}
}
timeout = opts[:timeout] || @default_timeout
call_opts = [timeout: timeout]
result = Bridge.BridgeService.Stub.register_variable(channel, request, call_opts)
case result do
{:ok, response, _headers} ->
Logger.debug("Got response: #{inspect(response)}")
if response.success do
{:ok, %{success: true}}
else
{:error, response.error_message || "Failed to register variable"}
end
{:ok, response} ->
# The stub is returning just {:ok, response} without headers
if response.success do
{:ok, %{success: true}}
else
{:error, response.error_message || "Failed to register variable"}
end
error ->
handle_error(error)
end
error ->
error
end
end
def get_variable(channel, session_id, identifier, opts \\ []) do
request = %Bridge.GetVariableRequest{
session_id: session_id,
variable_identifier: to_string(identifier),
bypass_cache: opts[:bypass_cache] || false
}
timeout = opts[:timeout] || @default_timeout
call_opts = [timeout: timeout]
case Bridge.BridgeService.Stub.get_variable(channel, request, call_opts) do
{:ok, response, _headers} ->
if response.variable do
{:ok, decode_variable(response.variable)}
else
{:error, :not_found}
end
{:ok, response} ->
# Handle 2-tuple response
if response.variable do
{:ok, decode_variable(response.variable)}
else
{:error, :not_found}
end
error ->
handle_error(error)
end
end
def set_variable(channel, session_id, name, value, opts \\ []) do
# Infer type from current variable
case get_variable(channel, session_id, name) do
{:ok, current_var} ->
# Encode value with the variable's type (type is already an atom)
case Serialization.encode_any(value, current_var.type) do
{:ok, any_value, _binary_data} ->
proto_any = %Google.Protobuf.Any{
type_url: any_value.type_url,
value: any_value.value
}
request = %Bridge.SetVariableRequest{
session_id: session_id,
variable_identifier: to_string(name),
value: proto_any,
metadata: opts[:metadata] || %{},
expected_version: opts[:expected_version] || 0
}
timeout = opts[:timeout] || @default_timeout
call_opts = [timeout: timeout]
case Bridge.BridgeService.Stub.set_variable(channel, request, call_opts) do
{:ok, response, _headers} ->
if response.success do
{:ok, %{success: true}}
else
{:error, response.error || "Failed to set variable"}
end
{:ok, response} ->
# Handle 2-tuple response
if response.success do
{:ok, %{success: true}}
else
{:error, response.error || "Failed to set variable"}
end
error ->
handle_error(error)
end
error ->
error
end
error ->
error
end
end
def list_variables(channel, session_id, pattern \\ "*", opts \\ []) do
request = %Bridge.ListVariablesRequest{
session_id: session_id,
pattern: pattern
}
timeout = opts[:timeout] || @default_timeout
call_opts = [timeout: timeout]
case Bridge.BridgeService.Stub.list_variables(channel, request, call_opts) do
{:ok, response, _headers} ->
variables = Enum.map(response.variables, &decode_variable/1)
{:ok, %{variables: variables}}
{:ok, response} ->
# Handle 2-tuple response
variables = Enum.map(response.variables, &decode_variable/1)
{:ok, %{variables: variables}}
error ->
handle_error(error)
end
end
def delete_variable(channel, session_id, identifier, opts \\ []) do
request = %Bridge.DeleteVariableRequest{
session_id: session_id,
variable_identifier: to_string(identifier)
}
timeout = opts[:timeout] || @default_timeout
call_opts = [timeout: timeout]
case Bridge.BridgeService.Stub.delete_variable(channel, request, call_opts) do
{:ok, response, _headers} ->
if response.success do
{:ok, %{success: true}}
else
{:error, response.error_message || "Failed to delete variable"}
end
{:ok, response} ->
# Handle 2-tuple response
if response.success do
{:ok, %{success: true}}
else
{:error, response.error_message || "Failed to delete variable"}
end
error ->
handle_error(error)
end
end
def get_session(channel, session_id, opts \\ []) do
request = %Bridge.GetSessionRequest{
session_id: session_id
}
timeout = opts[:timeout] || @default_timeout
call_opts = [timeout: timeout]
case Bridge.BridgeService.Stub.get_session(channel, request, call_opts) do
{:ok, response, _headers} ->
if response.session_id do
{:ok, %{session: decode_session_response(response)}}
else
{:error, :not_found}
end
{:ok, response} ->
# Handle 2-tuple response
if response.session_id do
{:ok, %{session: decode_session_response(response)}}
else
{:error, :not_found}
end
error ->
handle_error(error)
end
end
def heartbeat(channel, session_id, opts \\ []) do
request = %Bridge.HeartbeatRequest{
session_id: session_id
}
timeout = opts[:timeout] || @default_timeout
call_opts = [timeout: timeout]
case Bridge.BridgeService.Stub.heartbeat(channel, request, call_opts) do
{:ok, response, _headers} ->
{:ok, %{success: response.session_valid}}
{:ok, response} ->
# Handle 2-tuple response
{:ok, %{success: response.session_valid}}
error ->
handle_error(error)
end
end
def set_variables(channel, session_id, updates, opts \\ []) do
# Convert updates to map format expected by proto
updates_map =
Enum.into(updates, %{}, fn update ->
# Get current type for encoding
{:ok, current_var} = get_variable(channel, session_id, update.identifier.name)
{:ok, any_value, _binary_data} = Serialization.encode_any(update.value, current_var.type)
{update.identifier.name,
%Google.Protobuf.Any{
type_url: any_value.type_url,
value: any_value.value
}}
end)
request = %Bridge.BatchSetVariablesRequest{
session_id: session_id,
updates: updates_map,
metadata: opts[:metadata] || %{},
atomic: opts[:atomic] || false
}
timeout = opts[:timeout] || @default_timeout
call_opts = [timeout: timeout]
case Bridge.BridgeService.Stub.set_variables(channel, request, call_opts) do
{:ok, response, _headers} ->
handle_batch_response(response, updates)
{:ok, response} ->
# Handle 2-tuple response
handle_batch_response(response, updates)
error ->
handle_error(error)
end
end
defp handle_batch_response(response, updates) do
if response.success do
# Convert the response to expected format
results =
Enum.map(updates, fn update ->
error = Map.get(response.errors || %{}, update.identifier.name)
%{
identifier: update.identifier,
success: is_nil(error),
error: error
}
end)
{:ok, %{results: results}}
else
{:error, "Batch update failed"}
end
end
def watch_variables(channel, session_id, names, opts \\ []) do
request = %Bridge.WatchVariablesRequest{
session_id: session_id,
variable_identifiers: names,
include_initial_values: opts[:include_initial] || false
}
# 5 minutes for streaming
timeout = opts[:timeout] || 300_000
call_opts = [timeout: timeout]
case Bridge.BridgeService.Stub.watch_variables(channel, request, call_opts) do
stream when is_struct(stream, Enumerable.GRPC.Client.Stream) ->
{:ok, stream}
error ->
handle_error(error)
end
end
def execute_tool(channel, session_id, tool_name, parameters, opts \\ []) do
# Convert Elixir terms to protobuf Any messages
proto_params_result =
Enum.reduce_while(parameters, {:ok, %{}}, fn {k, v}, {:ok, acc} ->
case infer_and_encode_any(v) do
{:ok, proto_any} -> {:cont, {:ok, Map.put(acc, to_string(k), proto_any)}}
{:error, reason} -> {:halt, {:error, reason}}
end
end)
with {:ok, proto_params} <- proto_params_result do
request = %Bridge.ExecuteToolRequest{
session_id: session_id,
tool_name: tool_name,
parameters: proto_params
}
timeout = opts[:timeout] || @default_timeout
call_opts = [timeout: timeout]
case Bridge.BridgeService.Stub.execute_tool(channel, request, call_opts) do
{:ok, response, _headers} -> handle_tool_response(response)
{:ok, response} -> handle_tool_response(response)
{:error, reason} -> handle_error(reason)
end
end
end
def execute_streaming_tool(channel, session_id, tool_name, parameters, opts \\ []) do
# Convert Elixir terms to protobuf Any messages
Logger.info("[ClientImpl] execute_streaming_tool called: #{tool_name}")
with {:ok, proto_params} <- encode_parameters(parameters) do
request = %Bridge.ExecuteToolRequest{
session_id: session_id,
tool_name: tool_name,
parameters: proto_params,
stream: true
}
timeout = opts[:timeout] || 300_000
call_opts = [timeout: timeout]
# This call returns {:ok, Stream} or {:error, reason}
Logger.info("[ClientImpl] Calling Bridge.BridgeService.Stub.execute_streaming_tool")
result = Bridge.BridgeService.Stub.execute_streaming_tool(channel, request, call_opts)
Logger.info("[ClientImpl] Stub returned: #{inspect(result)}")
result
end
end
# Helper functions
defp decode_variable(proto_var) do
# Decode the Any value
{:ok, value} =
if proto_var.value do
Serialization.decode_any(%{
type_url: proto_var.value.type_url,
value: proto_var.value.value
})
else
{:ok, nil}
end
%{
id: proto_var.id,
name: proto_var.name,
type: String.to_atom(proto_var.type),
value: value,
version: proto_var.version,
source: proto_var.source,
# Not in proto
created_at: nil,
updated_at: proto_var.last_updated_at,
metadata: Map.new(proto_var.metadata),
constraints: decode_constraints(proto_var.constraints_json)
}
end
defp decode_constraints(""), do: %{}
defp decode_constraints(nil), do: %{}
defp decode_constraints(json_str) do
case Jason.decode(json_str) do
{:ok, constraints} -> constraints
_ -> %{}
end
end
defp decode_session_response(response) do
%{
id: response.session_id,
# Assume active if we got a response
active: true,
created_at: response.created_at,
# Not provided in response
last_activity: nil,
metadata: Map.new(response.metadata || %{})
}
end
defp handle_error({:error, %GRPC.RPCError{} = error}) do
Logger.error("gRPC error: #{inspect(error)}")
case error.status do
3 -> {:error, :invalid_argument}
5 -> {:error, :not_found}
13 -> {:error, :internal}
14 -> {:error, :unavailable}
_ -> {:error, error}
end
end
defp handle_error(error), do: error
# Helper to infer type and encode to Google.Protobuf.Any
defp infer_and_encode_any(value) do
type = Snakepit.Bridge.Variables.Types.infer_type(value)
# For complex types that would be encoded as :string, we need to handle them specially
if type == :string and
(is_map(value) or (is_list(value) and not Enum.all?(value, &is_number/1))) do
# Encode complex structures as JSON strings
json_value = Jason.encode!(value)
with {:ok, any_struct, _binary_data} <- Serialization.encode_any(json_value, :string) do
{:ok, %Google.Protobuf.Any{type_url: any_struct.type_url, value: any_struct.value}}
end
else
# Use the serialization module for other types
with {:ok, any_struct, _binary_data} <- Serialization.encode_any(value, type) do
{:ok, %Google.Protobuf.Any{type_url: any_struct.type_url, value: any_struct.value}}
end
end
end
defp handle_tool_response(%Bridge.ExecuteToolResponse{success: true, result: any_result}) do
# Use the centralized decoder - this is the correct approach
Serialization.decode_any(any_result)
end
defp handle_tool_response(%Bridge.ExecuteToolResponse{success: false, error_message: error}) do
{:error, error}
end
defp encode_parameters(parameters) do
Enum.reduce_while(parameters, {:ok, %{}}, fn {k, v}, {:ok, acc} ->
case infer_and_encode_any(v) do
{:ok, proto_any} -> {:cont, {:ok, Map.put(acc, to_string(k), proto_any)}}
{:error, reason} -> {:halt, {:error, reason}}
end
end)
end
end