Skip to content

Commit bf1d3c2

Browse files
authored
Merge pull request #1137 from pipermerriam/piper/filters-for-peer-pool-subscribers
PeerSubscribers now specify what messages they are interested in
2 parents 888f105 + 0fb8a71 commit bf1d3c2

File tree

9 files changed

+205
-47
lines changed

9 files changed

+205
-47
lines changed

p2p/peer.py

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import collections
33
import contextlib
44
import datetime
5+
import functools
56
import logging
67
import operator
78
import random
@@ -20,6 +21,7 @@
2021
Dict,
2122
Iterator,
2223
List,
24+
Set,
2325
TYPE_CHECKING,
2426
Tuple,
2527
Type,
@@ -381,14 +383,23 @@ def handle_p2p_msg(self, cmd: protocol.Command, msg: protocol._DecodedMsgType) -
381383
raise UnexpectedMessage("Unexpected msg: {} ({})".format(cmd, msg))
382384

383385
def handle_sub_proto_msg(self, cmd: protocol.Command, msg: protocol._DecodedMsgType) -> None:
386+
cmd_type = type(cmd)
387+
384388
if self._subscribers:
385-
for subscriber in self._subscribers:
389+
was_added = tuple(
386390
subscriber.add_msg((self, cmd, msg))
391+
for subscriber
392+
in self._subscribers
393+
)
394+
if not any(was_added):
395+
self.logger.warn(
396+
"Peer %s has no subscribers for msg type %s",
397+
self,
398+
cmd_type.__name__,
399+
)
387400
else:
388401
self.logger.warn("Peer %s has no subscribers, discarding %s msg", self, cmd)
389402

390-
cmd_type = type(cmd)
391-
392403
if cmd_type in self.pending_requests:
393404
request, future = self.pending_requests[cmd_type]
394405
try:
@@ -547,10 +558,32 @@ def __hash__(self) -> int:
547558
class PeerSubscriber(ABC):
548559
_msg_queue: 'asyncio.Queue[PEER_MSG_TYPE]' = None
549560

561+
@property
562+
@abstractmethod
563+
def subscription_msg_types(self) -> Set[Type[protocol.Command]]:
564+
"""
565+
The `p2p.protocol.Command` types that this class subscribes to. Any
566+
command which is not in this set will not be passed to this subscriber.
567+
568+
The base command class `p2p.protocol.Command` can be used to enable
569+
**all** command types.
570+
571+
.. note: This API only applies to sub-protocol commands. Base protocol
572+
commands are handled exclusively at the peer level and cannot be
573+
consumed with this API.
574+
"""
575+
pass
576+
577+
@functools.lru_cache(maxsize=64)
578+
def is_subscription_command(self, cmd_type: Type[protocol.Command]) -> bool:
579+
return bool(self.subscription_msg_types.intersection(
580+
{cmd_type, protocol.Command}
581+
))
582+
550583
@property
551584
@abstractmethod
552585
def msg_queue_maxsize(self) -> int:
553-
raise NotImplementedError("Must be implemented by subclasses")
586+
pass
554587

555588
def register_peer(self, peer: BasePeer) -> None:
556589
"""
@@ -577,16 +610,27 @@ def msg_queue(self) -> 'asyncio.Queue[PEER_MSG_TYPE]':
577610
def queue_size(self) -> int:
578611
return self.msg_queue.qsize()
579612

580-
def add_msg(self, msg: 'PEER_MSG_TYPE') -> None:
613+
def add_msg(self, msg: 'PEER_MSG_TYPE') -> bool:
581614
peer, cmd, _ = msg
615+
616+
if not self.is_subscription_command(type(cmd)):
617+
self.logger.trace( # type: ignore
618+
"Discarding %s msg from %s; not subscribed to msg type; "
619+
"subscriptions: %s",
620+
cmd, peer, self.subscription_msg_types,
621+
)
622+
return False
623+
582624
try:
583625
self.logger.trace( # type: ignore
584626
"Adding %s msg from %s to queue; queue_size=%d", cmd, peer, self.queue_size)
585627
self.msg_queue.put_nowait(msg)
628+
return True
586629
except asyncio.queues.QueueFull:
587630
self.logger.warn( # type: ignore
588631
"%s msg queue is full; discarding %s msg from %s",
589632
self.__class__.__name__, cmd, peer)
633+
return False
590634

591635
@contextlib.contextmanager
592636
def subscribe(self, peer_pool: 'PeerPool') -> Iterator[None]:

tests/p2p/test_peer_subscriber.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import asyncio
2+
import logging
3+
4+
import pytest
5+
6+
from p2p.peer import PeerSubscriber
7+
from p2p.protocol import Command
8+
9+
from trinity.protocol.eth.peer import ETHPeer
10+
from trinity.protocol.eth.commands import GetBlockHeaders
11+
12+
from tests.trinity.core.peer_helpers import (
13+
get_directly_linked_peers,
14+
)
15+
16+
17+
logger = logging.getLogger('testing.p2p.PeerSubscriber')
18+
19+
20+
class HeadersSubscriber(PeerSubscriber):
21+
logger = logger
22+
msg_queue_maxsize = 10
23+
subscription_msg_types = {GetBlockHeaders}
24+
25+
26+
class AllSubscriber(PeerSubscriber):
27+
logger = logger
28+
msg_queue_maxsize = 10
29+
subscription_msg_types = {Command}
30+
31+
32+
@pytest.mark.asyncio
33+
async def test_peer_subscriber_filters_messages(request, event_loop):
34+
peer, remote = await get_directly_linked_peers(
35+
request,
36+
event_loop,
37+
peer1_class=ETHPeer,
38+
peer2_class=ETHPeer,
39+
)
40+
41+
header_subscriber = HeadersSubscriber()
42+
all_subscriber = AllSubscriber()
43+
44+
peer.add_subscriber(header_subscriber)
45+
peer.add_subscriber(all_subscriber)
46+
47+
remote.sub_proto.send_get_node_data([b'\x00' * 32])
48+
remote.sub_proto.send_get_block_headers(0, 1, 0, False)
49+
remote.sub_proto.send_get_node_data([b'\x00' * 32])
50+
remote.sub_proto.send_get_block_headers(1, 1, 0, False)
51+
remote.sub_proto.send_get_node_data([b'\x00' * 32])
52+
53+
# yeild to let remote and peer transmit.
54+
await asyncio.sleep(0.01)
55+
56+
assert header_subscriber.queue_size == 2
57+
assert all_subscriber.queue_size == 5

tests/trinity/core/peer_helpers.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import asyncio
22
import os
3-
from typing import List
3+
from typing import (
4+
List,
5+
)
46

57
from eth_hash.auto import keccak
68

@@ -16,6 +18,7 @@
1618
from p2p import kademlia
1719
from p2p.auth import decode_authentication
1820
from p2p.peer import BasePeer, PeerPool, PeerSubscriber
21+
from p2p.protocol import Command
1922

2023

2124
from trinity.protocol.les.peer import LESPeer
@@ -174,6 +177,8 @@ async def _run(self) -> None:
174177
class SamplePeerSubscriber(PeerSubscriber):
175178
logger = TraceLogger("")
176179

180+
subscription_msg_types = {Command}
181+
177182
@property
178183
def msg_queue_maxsize(self) -> int:
179184
return 100

trinity/plugins/builtin/tx_pool/pool.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
cast,
33
Callable,
44
Iterable,
5-
List
5+
List,
6+
Set,
7+
Type,
68
)
79
import uuid
810

@@ -21,6 +23,7 @@
2123
PeerPool,
2224
PeerSubscriber,
2325
)
26+
from p2p.protocol import Command
2427
from p2p.service import (
2528
BaseService
2629
)
@@ -59,12 +62,12 @@ def __init__(self,
5962
self._bloom = BloomFilter(max_elements=1000000)
6063
self._bloom_salt = str(uuid.uuid4())
6164

62-
@property
63-
def msg_queue_maxsize(self) -> int:
64-
# This is a rather arbitrary value, but when the sync is operating normally we never see
65-
# the msg queue grow past a few hundred items, so this should be a reasonable limit for
66-
# now.
67-
return 2000
65+
subscription_msg_types: Set[Type[Command]] = {Transactions}
66+
67+
# This is a rather arbitrary value, but when the sync is operating normally we never see
68+
# the msg queue grow past a few hundred items, so this should be a reasonable limit for
69+
# now.
70+
msg_queue_maxsize: int = 2000
6871

6972
async def _run(self) -> None:
7073
self.logger.info("Running Tx Pool")
@@ -74,8 +77,8 @@ async def _run(self) -> None:
7477
peer, cmd, msg = await self.wait(
7578
self.msg_queue.get(), token=self.cancel_token)
7679
peer = cast(ETHPeer, peer)
77-
msg = cast(List[BaseTransactionFields], msg)
7880
if isinstance(cmd, Transactions):
81+
msg = cast(List[BaseTransactionFields], msg)
7982
await self._handle_tx(peer, msg)
8083

8184
async def _handle_tx(self, peer: ETHPeer, txs: List[BaseTransactionFields]) -> None:

trinity/sync/full/chain.py

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
Dict,
77
List,
88
NamedTuple,
9+
Set,
910
Tuple,
11+
Type,
1012
Union,
1113
cast,
1214
)
@@ -32,9 +34,15 @@
3234
from p2p.exceptions import NoEligiblePeers
3335
from p2p.p2p_proto import DisconnectReason
3436
from p2p.peer import PeerPool
37+
from p2p.protocol import Command
3538

3639
from trinity.db.chain import AsyncChainDB
40+
from trinity.protocol.eth import commands
41+
from trinity.protocol.eth import (
42+
constants as eth_constants,
43+
)
3744
from trinity.protocol.eth.peer import ETHPeer
45+
from trinity.protocol.eth.requests import HeaderRequest
3846
from trinity.protocol.les.peer import LESPeer
3947
from trinity.rlp.block_body import BlockBody
4048
from trinity.sync.base_chain_syncer import BaseHeaderChainSyncer
@@ -66,6 +74,24 @@ def __init__(self,
6674
self._downloaded_receipts: asyncio.Queue[Tuple[ETHPeer, List[DownloadedBlockPart]]] = asyncio.Queue() # noqa: E501
6775
self._downloaded_bodies: asyncio.Queue[Tuple[ETHPeer, List[DownloadedBlockPart]]] = asyncio.Queue() # noqa: E501
6876

77+
subscription_msg_types: Set[Type[Command]] = {
78+
commands.BlockBodies,
79+
commands.Receipts,
80+
commands.NewBlock,
81+
commands.GetBlockHeaders,
82+
commands.BlockHeaders,
83+
commands.GetBlockBodies,
84+
commands.GetReceipts,
85+
commands.GetNodeData,
86+
commands.Transactions,
87+
commands.NodeData,
88+
# TODO: all of the following are here to quiet warning logging output
89+
# until the messages are properly handled.
90+
commands.Transactions,
91+
commands.NewBlock,
92+
commands.NewBlockHashes,
93+
}
94+
6995
async def _calculate_td(self, headers: Tuple[BlockHeader, ...]) -> int:
7096
"""Return the score (total difficulty) of the last header in the given list.
7197
@@ -191,7 +217,6 @@ def _request_block_parts(
191217
target_td: int,
192218
headers: List[BlockHeader],
193219
request_func: Callable[[ETHPeer, List[BlockHeader]], None]) -> int:
194-
from trinity.protocol.eth.peer import ETHPeer # noqa: F811
195220
peers = self.peer_pool.get_peers(target_td)
196221
if not peers:
197222
raise NoEligiblePeers()
@@ -235,15 +260,14 @@ def request_receipts(self, target_td: int, headers: List[BlockHeader]) -> int:
235260

236261
async def _handle_msg(self, peer: HeaderRequestingPeer, cmd: protocol.Command,
237262
msg: protocol._DecodedMsgType) -> None:
238-
from trinity.protocol.eth.peer import ETHPeer # noqa: F811
239-
from trinity.protocol.eth import commands
240-
from trinity.protocol.eth import (
241-
constants as eth_constants,
242-
)
243-
244263
peer = cast(ETHPeer, peer)
245264

246-
if isinstance(cmd, commands.BlockBodies):
265+
# TODO: stop ignoring these once we have proper handling for these messages.
266+
ignored_commands = (commands.Transactions, commands.NewBlock, commands.NewBlockHashes)
267+
268+
if isinstance(cmd, ignored_commands):
269+
pass
270+
elif isinstance(cmd, commands.BlockBodies):
247271
await self._handle_block_bodies(peer, list(cast(Tuple[BlockBody], msg)))
248272
elif isinstance(cmd, commands.Receipts):
249273
await self._handle_block_receipts(peer, cast(List[List[Receipt]], msg))
@@ -318,8 +342,6 @@ async def _handle_get_block_headers(
318342
self,
319343
peer: ETHPeer,
320344
query: Dict[str, Any]) -> None:
321-
from trinity.protocol.eth.requests import HeaderRequest # noqa: F811
322-
323345
self.logger.debug("Peer %s made header request: %s", peer, query)
324346
request = HeaderRequest(
325347
query['block_number_or_hash'],

trinity/sync/full/state.py

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
List,
1212
Set,
1313
Tuple,
14+
Type,
1415
Union,
1516
)
1617

@@ -88,12 +89,26 @@ def __init__(self,
8889
self._peer_missing_nodes: Dict[ETHPeer, Set[Hash32]] = collections.defaultdict(set)
8990
self._executor = get_asyncio_executor()
9091

91-
@property
92-
def msg_queue_maxsize(self) -> int:
93-
# This is a rather arbitrary value, but when the sync is operating normally we never see
94-
# the msg queue grow past a few hundred items, so this should be a reasonable limit for
95-
# now.
96-
return 2000
92+
# Throughout the whole state sync our chain head is fixed, so it makes sense to ignore
93+
# messages related to new blocks/transactions, but we must handle requests for data from
94+
# other peers or else they will disconnect from us.
95+
subscription_msg_types: Set[Type[Command]] = {
96+
commands.NodeData,
97+
commands.GetBlockHeaders,
98+
commands.GetBlockBodies,
99+
commands.GetReceipts,
100+
commands.GetNodeData,
101+
# TODO: all of the following are here to quiet warning logging output
102+
# until the messages are properly handled.
103+
commands.Transactions,
104+
commands.NewBlock,
105+
commands.NewBlockHashes,
106+
}
107+
108+
# This is a rather arbitrary value, but when the sync is operating normally we never see
109+
# the msg queue grow past a few hundred items, so this should be a reasonable limit for
110+
# now.
111+
msg_queue_maxsize: int = 2000
97112

98113
def deregister_peer(self, peer: BasePeer) -> None:
99114
# Use .pop() with a default value as it's possible we never requested anything to this
@@ -154,10 +169,9 @@ async def _process_nodes(self, nodes: Iterable[Tuple[Hash32, bytes]]) -> None:
154169

155170
async def _handle_msg(
156171
self, peer: ETHPeer, cmd: Command, msg: _DecodedMsgType) -> None:
157-
# Throughout the whole state sync our chain head is fixed, so it makes sense to ignore
158-
# messages related to new blocks/transactions, but we must handle requests for data from
159-
# other peers or else they will disconnect from us.
172+
# TODO: stop ignoring these once we have proper handling for these messages.
160173
ignored_commands = (commands.Transactions, commands.NewBlock, commands.NewBlockHashes)
174+
161175
if isinstance(cmd, ignored_commands):
162176
pass
163177
elif isinstance(cmd, commands.NodeData):

0 commit comments

Comments
 (0)