Hermolaos.Client.RequestTracker (Hermolaos v0.3.0)

View Source

Tracks pending JSON-RPC requests and correlates them with responses.

The RequestTracker maintains a mapping of request IDs to caller information, enabling the Connection to route responses back to the correct caller.

Features

  • Monotonically increasing integer IDs for efficiency
  • ETS-backed storage for O(1) lookups and concurrent access
  • Automatic timeout handling with configurable defaults
  • Statistics tracking for monitoring

Design Notes

This module uses ETS for storage because:

  1. Performance: O(1) lookups regardless of pending request count
  2. Concurrency: ETS tables support concurrent reads without locking
  3. Isolation: Each tracker has its own table, crashes don't affect others

Example

{:ok, tracker} = Hermolaos.Client.RequestTracker.start_link(timeout: 30_000)

# Track a request
id = Hermolaos.Client.RequestTracker.next_id(tracker)
:ok = Hermolaos.Client.RequestTracker.track(tracker, id, "tools/list", from)

# When response arrives, complete the request
{:ok, from, method} = Hermolaos.Client.RequestTracker.complete(tracker, id)
GenServer.reply(from, result)

Summary

Functions

Cancels a pending request.

Returns a specification to start this module under a supervisor.

Completes a pending request successfully.

Fails a pending request with an error.

Fails all pending requests (e.g., when connection closes).

Gets the next request ID (monotonically increasing integer).

Checks if a request ID is currently pending.

Returns the number of pending requests.

Starts a new request tracker.

Returns tracker statistics.

Types

from()

@type from() :: GenServer.from()

id()

@type id() :: integer()

method()

@type method() :: String.t()

pending_request()

@type pending_request() :: %{
  method: method(),
  from: from(),
  timeout_ref: reference() | nil,
  started_at: integer()
}

stats()

@type stats() :: %{
  requests_tracked: non_neg_integer(),
  requests_completed: non_neg_integer(),
  requests_failed: non_neg_integer(),
  requests_timed_out: non_neg_integer(),
  requests_cancelled: non_neg_integer()
}

t()

@type t() :: GenServer.server()

Functions

cancel(tracker, id)

@spec cancel(t(), id()) :: :ok

Cancels a pending request.

Examples

:ok = Hermolaos.Client.RequestTracker.cancel(tracker, 1)

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

complete(tracker, id)

@spec complete(t(), id()) :: {:ok, from(), method()} | {:error, :not_found}

Completes a pending request successfully.

Returns the original caller's from and method so the caller can be notified.

Examples

case Hermolaos.Client.RequestTracker.complete(tracker, 1) do
  {:ok, from, "tools/list"} ->
    GenServer.reply(from, {:ok, result})

  {:error, :not_found} ->
    # Request already completed, timed out, or never existed
    :ok
end

fail(tracker, id, error)

@spec fail(t(), id(), term()) :: {:ok, from(), method()} | {:error, :not_found}

Fails a pending request with an error.

Examples

case Hermolaos.Client.RequestTracker.fail(tracker, 1, error) do
  {:ok, from, method} ->
    GenServer.reply(from, {:error, error})

  {:error, :not_found} ->
    :ok
end

fail_all(tracker, error)

@spec fail_all(t(), term()) :: [{from(), method()}]

Fails all pending requests (e.g., when connection closes).

Returns the list of failed requests with their callers.

Examples

failed = Hermolaos.Client.RequestTracker.fail_all(tracker, {:error, :connection_closed})
for {from, method} <- failed do
  GenServer.reply(from, {:error, :connection_closed})
end

next_id(tracker)

@spec next_id(t()) :: id()

Gets the next request ID (monotonically increasing integer).

Examples

id = Hermolaos.Client.RequestTracker.next_id(tracker)
# => 1

id = Hermolaos.Client.RequestTracker.next_id(tracker)
# => 2

pending?(tracker, id)

@spec pending?(t(), id()) :: boolean()

Checks if a request ID is currently pending.

pending_count(tracker)

@spec pending_count(t()) :: non_neg_integer()

Returns the number of pending requests.

Examples

count = Hermolaos.Client.RequestTracker.pending_count(tracker)
# => 5

start_link(opts \\ [])

@spec start_link(keyword()) :: {:ok, pid()} | {:error, term()}

Starts a new request tracker.

Options

  • :timeout - Default timeout for requests in ms (default: 30000)
  • :name - GenServer name (optional)

Examples

{:ok, tracker} = Hermolaos.Client.RequestTracker.start_link()
{:ok, tracker} = Hermolaos.Client.RequestTracker.start_link(timeout: 60_000)

stats(tracker)

@spec stats(t()) :: stats()

Returns tracker statistics.

Examples

stats = Hermolaos.Client.RequestTracker.stats(tracker)
# => %{requests_tracked: 100, requests_completed: 95, ...}

track(tracker, id, method, from, timeout \\ nil)

@spec track(t(), id(), method(), from(), timeout() | nil) :: :ok

Tracks a pending request.

Parameters

  • tracker - The tracker process
  • id - Request ID (from next_id/1)
  • method - JSON-RPC method name
  • from - GenServer from tuple for reply
  • timeout - Optional timeout override in ms

Examples

:ok = Hermolaos.Client.RequestTracker.track(tracker, 1, "tools/list", from)
:ok = Hermolaos.Client.RequestTracker.track(tracker, 2, "tools/call", from, 60_000)