mirror of
https://github.com/Chia-Network/chia-blockchain.git
synced 2026-08-24 10:05:29 -05:00
CHIA-4017 Improve active requests tracking (#20748)
Improve active requests tracking.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
@@ -7,7 +8,7 @@ from typing import ClassVar, cast
|
||||
|
||||
import pytest
|
||||
from chia_rs.sized_bytes import bytes32
|
||||
from chia_rs.sized_ints import int16, uint32
|
||||
from chia_rs.sized_ints import int16, uint8, uint16, uint32
|
||||
from packaging.version import Version
|
||||
|
||||
from chia import __version__
|
||||
@@ -18,7 +19,7 @@ from chia._tests.util.time_out_assert import time_out_assert
|
||||
from chia.full_node.full_node_api import FullNodeAPI
|
||||
from chia.full_node.start_full_node import create_full_node_service
|
||||
from chia.protocols.full_node_protocol import RejectBlock, RequestBlock, RequestTransaction
|
||||
from chia.protocols.outbound_message import NodeType, make_msg
|
||||
from chia.protocols.outbound_message import Message, NodeType, make_msg
|
||||
from chia.protocols.protocol_message_types import ProtocolMessageTypes
|
||||
from chia.protocols.shared_protocol import Error, protocol_version
|
||||
from chia.protocols.wallet_protocol import RejectHeaderRequest
|
||||
@@ -27,6 +28,7 @@ from chia.server.server import ChiaServer
|
||||
from chia.server.ssl_context import chia_ssl_ca_paths, private_ssl_ca_paths
|
||||
from chia.server.ws_connection import WSChiaConnection, error_response_version, sanitize_version_string
|
||||
from chia.simulator.block_tools import BlockTools
|
||||
from chia.simulator.full_node_simulator import FullNodeSimulator
|
||||
from chia.types.peer_info import PeerInfo
|
||||
from chia.util.errors import ApiError, Err
|
||||
from chia.wallet.start_wallet import create_wallet_service
|
||||
@@ -305,3 +307,136 @@ async def test_connection_closed_banning(bt: BlockTools, caplog: pytest.LogCaptu
|
||||
trusted_peer = FakeConnection(peer_info=PeerInfo("34.34.34.34", 8444))
|
||||
await my_test_server.connection_closed(cast(WSChiaConnection, trusted_peer), ban_time=60)
|
||||
assert f"Trying to ban trusted peer {trusted_peer.peer_info.host} for 60, but will not ban" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.limit_consensus_modes(allowed=[ConsensusMode.HARD_FORK_2_0], reason="irrelevant")
|
||||
async def test_route_incoming_message(
|
||||
one_node_one_block: tuple[FullNodeSimulator, ChiaServer, BlockTools], self_hostname: str
|
||||
) -> None:
|
||||
"""
|
||||
Covers the scenarios where incoming messages are responses to active
|
||||
requests, responses to timed out requests (late) and the rest.
|
||||
"""
|
||||
_, server, _ = one_node_one_block
|
||||
wsc, _ = await add_dummy_connection_wsc(server, self_hostname, 1337)
|
||||
test_response_type = uint8(ProtocolMessageTypes.respond_block.value)
|
||||
# In time message gets handled via the response route
|
||||
response_event = asyncio.Event()
|
||||
pending_request_id = uint16(5)
|
||||
wsc.pending_requests[pending_request_id] = response_event
|
||||
in_time_msg = Message(type=test_response_type, id=pending_request_id, data=b"")
|
||||
await wsc._route_incoming_message(in_time_msg)
|
||||
assert wsc.request_results[pending_request_id] == in_time_msg
|
||||
assert response_event.is_set()
|
||||
# Timed out message gets dropped
|
||||
timed_out_request_id = uint16(6)
|
||||
wsc.timed_out_requests.add(timed_out_request_id)
|
||||
timed_out_msg = Message(type=test_response_type, id=timed_out_request_id, data=b"")
|
||||
incoming_queue_size = wsc.incoming_queue.qsize()
|
||||
await wsc._route_incoming_message(timed_out_msg)
|
||||
assert timed_out_request_id not in wsc.timed_out_requests
|
||||
# The incoming queue size doesn't increase as the message got dropped
|
||||
assert wsc.incoming_queue.qsize() == incoming_queue_size
|
||||
# Other messages are forwarded to the incoming queue
|
||||
other_msg = Message(test_response_type, uint16(7), b"")
|
||||
await wsc._route_incoming_message(other_msg)
|
||||
assert wsc.incoming_queue.qsize() == incoming_queue_size + 1
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.limit_consensus_modes(allowed=[ConsensusMode.HARD_FORK_2_0], reason="irrelevant")
|
||||
@pytest.mark.parametrize("is_outbound, range_start, range_end", [(True, 0, 2**15 - 1), (False, 2**15, 2**16 - 1)])
|
||||
async def test_select_request_nonce(
|
||||
one_node_one_block: tuple[FullNodeSimulator, ChiaServer, BlockTools],
|
||||
self_hostname: str,
|
||||
is_outbound: bool,
|
||||
range_start: int,
|
||||
range_end: int,
|
||||
) -> None:
|
||||
"""
|
||||
Covers the scenarios of skipping active nonces, selecting nonces that are
|
||||
available in range, wraps occurring and no available nonces. We control
|
||||
inbound/outbound via `is_outbound`.
|
||||
"""
|
||||
_, server, _ = one_node_one_block
|
||||
wsc, _ = await add_dummy_connection_wsc(server, self_hostname, 1337)
|
||||
wsc.is_outbound = is_outbound
|
||||
# Skip used nonces
|
||||
wsc.pending_requests[uint16(range_start)] = asyncio.Event()
|
||||
wsc.timed_out_requests.add(uint16(range_start + 1))
|
||||
wsc.request_nonce = uint16(range_start)
|
||||
assert wsc._select_request_nonce() == uint16(range_start + 2)
|
||||
assert wsc.request_nonce == uint16(range_start + 3)
|
||||
# Make sure that next selection skips properly if `request_nonce` points to
|
||||
# a reserved nonce after successful allocation.
|
||||
wsc.pending_requests.clear()
|
||||
wsc.timed_out_requests.clear()
|
||||
wsc.pending_requests[uint16(range_start)] = asyncio.Event()
|
||||
wsc.timed_out_requests.add(uint16(range_start + 1))
|
||||
wsc.pending_requests[uint16(range_start + 3)] = asyncio.Event()
|
||||
wsc.request_nonce = uint16(range_start)
|
||||
assert wsc._select_request_nonce() == uint16(range_start + 2)
|
||||
# This points to a reserved nonce
|
||||
assert wsc.request_nonce == uint16(range_start + 3)
|
||||
wsc.timed_out_requests.add(uint16(range_start + 4))
|
||||
# Next selection should skip the reserved nonce and the timed out nonce
|
||||
assert wsc._select_request_nonce() == uint16(range_start + 5)
|
||||
assert wsc.request_nonce == uint16(range_start + 6)
|
||||
# Nonce in range and unused
|
||||
wsc.request_nonce = uint16(range_start + 2)
|
||||
wsc.pending_requests.clear()
|
||||
wsc.timed_out_requests.clear()
|
||||
assert wsc._select_request_nonce() == uint16(range_start + 2)
|
||||
assert wsc.request_nonce == uint16(range_start + 3)
|
||||
# Wrap stays within inbound/outbound range
|
||||
wsc.pending_requests.clear()
|
||||
wsc.timed_out_requests.clear()
|
||||
wsc.pending_requests[uint16(range_end)] = asyncio.Event()
|
||||
wsc.request_nonce = uint16(range_end)
|
||||
assert wsc._select_request_nonce() == uint16(range_start)
|
||||
assert wsc.request_nonce == uint16(range_start + 1)
|
||||
# No nonces available
|
||||
wsc.pending_requests.clear()
|
||||
wsc.timed_out_requests.clear()
|
||||
for nonce in range(range_start, range_end + 1):
|
||||
wsc.pending_requests[uint16(nonce)] = asyncio.Event()
|
||||
wsc.request_nonce = uint16(range_start)
|
||||
assert wsc._select_request_nonce() is None
|
||||
# Make sure we don't advance in this case
|
||||
assert wsc.request_nonce == uint16(range_start)
|
||||
# With no nonces currently available, send a request and make sure the
|
||||
# connection gets closed.
|
||||
msg = make_msg(ProtocolMessageTypes.request_block, RequestBlock(uint32(42), False))
|
||||
response = await wsc.send_request(message_no_id=msg, timeout=1)
|
||||
assert response is None
|
||||
assert wsc.closed
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.limit_consensus_modes(allowed=[ConsensusMode.HARD_FORK_2_0], reason="irrelevant")
|
||||
async def test_inbound_handler_none_msg(
|
||||
one_node_one_block: tuple[FullNodeSimulator, ChiaServer, BlockTools],
|
||||
self_hostname: str,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""
|
||||
Covers the scenario where the inbound handler receives `None` from
|
||||
`_read_one_message`.
|
||||
"""
|
||||
_, server, _ = one_node_one_block
|
||||
wsc, peer_id = await add_dummy_connection_wsc(server, self_hostname, 1337)
|
||||
await time_out_assert(5, lambda: peer_id in server.all_connections)
|
||||
read_calls = 0
|
||||
|
||||
async def test_read_one_message() -> Message | None:
|
||||
nonlocal read_calls
|
||||
read_calls += 1
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(wsc, "_read_one_message", test_read_one_message)
|
||||
assert wsc.inbound_task is not None
|
||||
await asyncio.wait_for(wsc.inbound_task, timeout=1)
|
||||
assert read_calls == 1
|
||||
assert wsc.incoming_queue.qsize() == 0
|
||||
await wsc.close()
|
||||
|
||||
@@ -126,6 +126,9 @@ class WSChiaConnection:
|
||||
|
||||
pending_requests: dict[uint16, asyncio.Event] = field(default_factory=dict, repr=False)
|
||||
request_results: dict[uint16, Message] = field(default_factory=dict, repr=False)
|
||||
# Nonces of `call_api` requests that timed out. Used to distinguish a late
|
||||
# response from an unsolicited one.
|
||||
timed_out_requests: set[uint16] = field(default_factory=set, repr=False)
|
||||
closed: bool = False
|
||||
connection_type: NodeType | None = None
|
||||
request_nonce: uint16 = uint16(0)
|
||||
@@ -530,19 +533,31 @@ class WSChiaConnection:
|
||||
api_task = create_referenced_task(self._api_call(message, task_id))
|
||||
self.api_tasks[task_id] = api_task
|
||||
|
||||
async def _route_incoming_message(self, message: Message) -> None:
|
||||
if message.id in self.pending_requests:
|
||||
self.request_results[message.id] = message
|
||||
self.pending_requests[message.id].set()
|
||||
return
|
||||
if message.id in self.timed_out_requests:
|
||||
# This is a late response for a timed out request. The caller is
|
||||
# expected to move on.
|
||||
self.timed_out_requests.discard(message.id)
|
||||
self.log.info(
|
||||
f"Ignoring late response {ProtocolMessageTypes(message.type).name} "
|
||||
f"from {self.peer_info.host} version {self.version}"
|
||||
)
|
||||
return
|
||||
await self.incoming_queue.put(message)
|
||||
|
||||
async def inbound_handler(self) -> None:
|
||||
try:
|
||||
while not self.closed:
|
||||
message = await self._read_one_message()
|
||||
if message is not None:
|
||||
if message.id in self.pending_requests:
|
||||
self.request_results[message.id] = message
|
||||
event = self.pending_requests[message.id]
|
||||
event.set()
|
||||
else:
|
||||
await self.incoming_queue.put(message)
|
||||
else:
|
||||
continue
|
||||
if message is None:
|
||||
# This indicates a connection shutdown so there is no point
|
||||
# to continue.
|
||||
break
|
||||
await self._route_incoming_message(message)
|
||||
except asyncio.CancelledError:
|
||||
self.log.debug("Inbound_handler task cancelled")
|
||||
except Exception as e:
|
||||
@@ -601,6 +616,27 @@ class WSChiaConnection:
|
||||
assert receive_metadata is not None, f"ApiMetadata unavailable for {recv_method}"
|
||||
return receive_metadata.message_class.from_bytes(response.data)
|
||||
|
||||
def _next_request_nonce(self, nonce: uint16) -> uint16:
|
||||
if self.is_outbound:
|
||||
return uint16(nonce + 1) if nonce != uint16(2**15 - 1) else uint16(0)
|
||||
return uint16(nonce + 1) if nonce != uint16(2**16 - 1) else uint16(2**15)
|
||||
|
||||
def _select_request_nonce(self) -> uint16 | None:
|
||||
"""
|
||||
Allocate a nonce that is not used by a pending or a timed out request
|
||||
and advance `request_nonce` to the next candidate.
|
||||
"""
|
||||
start_nonce = self.request_nonce
|
||||
nonce_candidate = start_nonce
|
||||
while True:
|
||||
if nonce_candidate not in self.pending_requests and nonce_candidate not in self.timed_out_requests:
|
||||
self.request_nonce = self._next_request_nonce(nonce_candidate)
|
||||
return nonce_candidate
|
||||
nonce_candidate = self._next_request_nonce(nonce_candidate)
|
||||
# See if we're back to the start
|
||||
if nonce_candidate == start_nonce:
|
||||
return None
|
||||
|
||||
async def send_request(self, message_no_id: Message, timeout: int) -> Message | None:
|
||||
"""Sends a message and waits for a response."""
|
||||
if self.closed:
|
||||
@@ -611,11 +647,13 @@ class WSChiaConnection:
|
||||
|
||||
# The request nonce is an integer between 0 and 2**16 - 1, which is used to match requests to responses
|
||||
# If is_outbound, 0 <= nonce < 2^15, else 2^15 <= nonce < 2^16
|
||||
request_id = self.request_nonce
|
||||
if self.is_outbound:
|
||||
self.request_nonce = uint16(self.request_nonce + 1) if self.request_nonce != (2**15 - 1) else uint16(0)
|
||||
else:
|
||||
self.request_nonce = uint16(self.request_nonce + 1) if self.request_nonce != (2**16 - 1) else uint16(2**15)
|
||||
request_id = self._select_request_nonce()
|
||||
if request_id is None:
|
||||
self.log.info(
|
||||
f"Disconnecting peer {self.peer_info.host} version {self.version} for no available request nonces"
|
||||
)
|
||||
await self.close()
|
||||
return None
|
||||
|
||||
message = Message(message_no_id.type, request_id, message_no_id.data)
|
||||
assert message.id is not None
|
||||
@@ -627,16 +665,14 @@ class WSChiaConnection:
|
||||
except asyncio.TimeoutError:
|
||||
self.log.debug(f"Request timeout: {message}")
|
||||
|
||||
self.pending_requests.pop(message.id)
|
||||
result: Message | None = None
|
||||
if message.id in self.request_results:
|
||||
result = self.request_results[message.id]
|
||||
assert result is not None
|
||||
self.log.debug(
|
||||
f"<- {ProtocolMessageTypes(result.type).name} from: {self.peer_info.host}:{self.peer_info.port}"
|
||||
)
|
||||
self.request_results.pop(message.id)
|
||||
|
||||
result = self.request_results.pop(message.id, None)
|
||||
if result is None:
|
||||
# This request has timed out
|
||||
self.timed_out_requests.add(message.id)
|
||||
self.pending_requests.pop(message.id, None)
|
||||
return None
|
||||
self.log.debug(f"<- {ProtocolMessageTypes(result.type).name} from: {self.peer_info.host}:{self.peer_info.port}")
|
||||
self.pending_requests.pop(message.id, None)
|
||||
return result
|
||||
|
||||
async def _wait_and_retry(self, msg: Message) -> None:
|
||||
|
||||
Reference in New Issue
Block a user