Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom timeout for requests #687

Merged
merged 11 commits into from
May 7, 2024
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added support for `XChainModifyBridge` flag maps (fixing an issue with `NFTokenCreateOffer` flag names)
- Fixed `XChainModifyBridge` validation to allow just clearing of `MinAccountCreateAmount`
- Currency codes with special characters not being allowed by IssuedCurrency objects.
- Specify custom timeout parameter for requests.

## [2.5.0] - 2023-11-30

Expand Down
8 changes: 5 additions & 3 deletions xrpl/asyncio/clients/async_websocket_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from collections.abc import AsyncIterator
from types import TracebackType
from typing import Any, Dict, Type
from typing import Any, Dict, Optional, Type

from xrpl.asyncio.clients.async_client import AsyncClient
from xrpl.asyncio.clients.exceptions import XRPLWebsocketException
Expand Down Expand Up @@ -262,7 +262,9 @@ async def send(self: AsyncWebsocketClient, request: Request) -> None:
raise XRPLWebsocketException("Websocket is not open")
await self._do_send(request)

async def _request_impl(self: WebsocketBase, request: Request) -> Response:
async def _request_impl(
self: WebsocketBase, request: Request, timeout: Optional[float] = None
ckeshava marked this conversation as resolved.
Show resolved Hide resolved
) -> Response:
"""
``_request_impl`` implementation for async websocket.

Expand All @@ -280,4 +282,4 @@ async def _request_impl(self: WebsocketBase, request: Request) -> Response:
"""
if not self.is_open():
raise XRPLWebsocketException("Websocket is not open")
return await self._do_request_impl(request)
return await self._do_request_impl(request, timeout)
16 changes: 15 additions & 1 deletion xrpl/asyncio/clients/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,15 @@
from abc import ABC, abstractmethod
from typing import Optional

from typing_extensions import Final

from xrpl.models.requests.request import Request
from xrpl.models.response import Response

# The default request timeout duration. If you need more time, please pass the required
ckeshava marked this conversation as resolved.
Show resolved Hide resolved
# value into the Client._request_impl function
_TIMEOUT: Final[float] = 10.0


class Client(ABC):
"""
Expand All @@ -27,14 +33,22 @@ def __init__(self: Client, url: str) -> None:
self.build_version: Optional[str] = None

@abstractmethod
async def _request_impl(self: Client, request: Request) -> Response:
async def _request_impl(
self: Client, request: Request, timeout: Optional[float] = _TIMEOUT
) -> Response:
"""
This is the actual driver for a given Client's request. It must be
async because all of the helper functions in this library are
async-first. Implement this in a given Client.

Arguments:
request: An object representing information about a rippled request.
timeout: The maximum tolerable delay on waiting for a response.
Note: Optional is used in the type in order to honor the existing
behavior in certain overridden functions. WebsocketBase.do_request_impl
waits indefinitely for the completion of a request, whereas
JsonRpcBase._request_impl waits for 10 seconds before timing out a
request.
ckeshava marked this conversation as resolved.
Show resolved Hide resolved

Returns:
The response from the server, as a Response object.
Expand Down
14 changes: 8 additions & 6 deletions xrpl/asyncio/clients/json_rpc_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,16 @@
from __future__ import annotations

from json import JSONDecodeError
from typing import Optional

from httpx import AsyncClient
from typing_extensions import Final

from xrpl.asyncio.clients.client import Client
from xrpl.asyncio.clients.client import _TIMEOUT, Client
from xrpl.asyncio.clients.exceptions import XRPLRequestFailureException
from xrpl.asyncio.clients.utils import json_to_response, request_to_json_rpc
from xrpl.models.requests.request import Request
from xrpl.models.response import Response

_TIMEOUT: Final[float] = 10.0


class JsonRpcBase(Client):
"""
Expand All @@ -22,12 +20,16 @@ class JsonRpcBase(Client):
:meta private:
"""

async def _request_impl(self: JsonRpcBase, request: Request) -> Response:
async def _request_impl(
self: JsonRpcBase, request: Request, timeout: Optional[float] = _TIMEOUT
) -> Response:
"""
Base ``_request_impl`` implementation for JSON RPC.

Arguments:
request: An object representing information about a rippled request.
timeout: The duration within which we expect to hear a response from the
rippled validator.

Returns:
The response from the server, as a Response object.
Expand All @@ -37,7 +39,7 @@ async def _request_impl(self: JsonRpcBase, request: Request) -> Response:

:meta private:
"""
async with AsyncClient(timeout=_TIMEOUT) as http_client:
async with AsyncClient(timeout=timeout) as http_client:
response = await http_client.post(
self.url,
json=request_to_json_rpc(request),
Expand Down
16 changes: 12 additions & 4 deletions xrpl/asyncio/clients/websocket_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,9 @@ async def _do_pop_message(self: WebsocketBase) -> Dict[str, Any]:
cast(_MESSAGES_TYPE, self._messages).task_done()
return msg

async def _do_request_impl(self: WebsocketBase, request: Request) -> Response:
async def _do_request_impl(
self: WebsocketBase, request: Request, timeout: Optional[float]
justinr1234 marked this conversation as resolved.
Show resolved Hide resolved
) -> Response:
"""
Base ``_request_impl`` implementation for websockets.

Expand All @@ -221,8 +223,14 @@ async def _do_request_impl(self: WebsocketBase, request: Request) -> Response:

# fire-and-forget the send, and await the Future
asyncio.create_task(self._do_send_no_future(request_with_id))
raw_response = await self._open_requests[request_str]

# remove the resolved Future, hopefully getting it garbage colleted
del self._open_requests[request_str]
try:
raw_response = await asyncio.wait_for(
self._open_requests[request_str], timeout
)
finally:
# remove the resolved Future, hopefully getting it garbage colleted
# Ensure the request is removed whether it times out or not
del self._open_requests[request_str]

return websocket_to_response(raw_response)
6 changes: 4 additions & 2 deletions xrpl/clients/websocket_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,9 @@ def send(self: WebsocketClient, request: Request) -> None:
self._do_send(request), cast(asyncio.AbstractEventLoop, self._loop)
).result()

async def _request_impl(self: WebsocketClient, request: Request) -> Response:
async def _request_impl(
self: WebsocketClient, request: Request, timeout: Optional[float] = None
) -> Response:
"""
``_request_impl`` implementation for sync websockets that ensures the
``WebsocketBase._do_request_impl`` implementation is run on the other thread.
Expand Down Expand Up @@ -232,6 +234,6 @@ async def _request_impl(self: WebsocketClient, request: Request) -> Response:
# completely block the main thread until completed,
# just as if it were not async.
return asyncio.run_coroutine_threadsafe(
self._do_request_impl(request),
self._do_request_impl(request, timeout),
cast(asyncio.AbstractEventLoop, self._loop),
).result()
Loading