1
1
import asyncio
2
2
from abc import abstractmethod
3
3
from typing import (
4
- Any ,
5
4
AsyncGenerator ,
6
5
Tuple ,
7
6
Union ,
15
14
from eth .exceptions import (
16
15
HeaderNotFound ,
17
16
)
17
+ from eth_typing import (
18
+ Hash32 ,
19
+ )
18
20
from eth_utils import (
19
21
ValidationError ,
20
22
)
21
23
from eth .rlp .headers import BlockHeader
22
24
23
25
from p2p import protocol
24
26
from p2p .constants import MAX_REORG_DEPTH , SEAL_CHECK_RANDOM_SAMPLE_RATE
25
- from p2p .exceptions import NoEligiblePeers
26
27
from p2p .p2p_proto import DisconnectReason
27
28
from p2p .peer import BasePeer , PeerPool , PeerSubscriber
28
29
from p2p .service import BaseService
@@ -45,19 +46,26 @@ class BaseHeaderChainSyncer(BaseService, PeerSubscriber):
45
46
"""
46
47
# We'll only sync if we are connected to at least min_peers_to_sync.
47
48
min_peers_to_sync = 1
48
- # Should we exit upon completing a sync with a given peer?
49
- _exit_on_sync_complete = False
49
+ # Post-processing steps can exit out of sync (for example, fast sync) by triggering this token:
50
+ complete_token = None
50
51
# TODO: Instead of a fixed timeout, we should use a variable one that gets adjusted based on
51
52
# the round-trip times from our download requests.
52
53
_reply_timeout = 60
53
54
_seal_check_random_sample_rate = SEAL_CHECK_RANDOM_SAMPLE_RATE
55
+ # the latest header hash of the peer on the current sync
56
+ _target_header_hash = None
54
57
55
58
def __init__ (self ,
56
59
chain : AsyncChain ,
57
60
db : AsyncHeaderDB ,
58
61
peer_pool : PeerPool ,
59
62
token : CancelToken = None ) -> None :
60
- super ().__init__ (token )
63
+ self .complete_token = CancelToken ('trinity.sync.common.BaseHeaderChainSyncer.SyncCompleted' )
64
+ if token is None :
65
+ super_service_token = self .complete_token
66
+ else :
67
+ super_service_token = token .chain (self .complete_token )
68
+ super ().__init__ (super_service_token )
61
69
self .chain = chain
62
70
self .db = db
63
71
self .peer_pool = peer_pool
@@ -66,13 +74,24 @@ def __init__(self,
66
74
self ._sync_complete = asyncio .Event ()
67
75
self ._sync_requests : asyncio .Queue [HeaderRequestingPeer ] = asyncio .Queue ()
68
76
77
+ # pending queue size should be big enough to avoid starving the processing consumers, but
78
+ # small enough to avoid wasteful over-requests before post-processing can happen
79
+ max_pending_headers = ETHPeer .max_headers_fetch * 5
80
+ self .pending_headers : asyncio .Queue [BlockHeader ] = asyncio .Queue (max_pending_headers )
81
+
69
82
@property
70
83
def msg_queue_maxsize (self ) -> int :
71
84
# This is a rather arbitrary value, but when the sync is operating normally we never see
72
85
# the msg queue grow past a few hundred items, so this should be a reasonable limit for
73
86
# now.
74
87
return 2000
75
88
89
+ def get_target_header_hash (self ) -> Hash32 :
90
+ if self ._target_header_hash is None :
91
+ raise ValueError ("Cannot check the target hash when there is no active sync" )
92
+ else :
93
+ return self ._target_header_hash
94
+
76
95
def register_peer (self , peer : BasePeer ) -> None :
77
96
self ._sync_requests .put_nowait (cast (HeaderRequestingPeer , self .peer_pool .highest_td_peer ))
78
97
@@ -99,19 +118,14 @@ async def _run(self) -> None:
99
118
self .run_task (self ._handle_msg_loop ())
100
119
with self .subscribe (self .peer_pool ):
101
120
while self .is_operational :
102
- peer_or_finished : Any = await self .wait_first (
103
- self ._sync_requests .get (),
104
- self ._sync_complete .wait ()
105
- )
106
-
107
- # In the case of a fast sync, we return once the sync is completed, and our caller
108
- # must then run the StateDownloader.
109
- if self ._sync_complete .is_set ():
121
+ try :
122
+ peer = await self .wait (self ._sync_requests .get ())
123
+ except OperationCancelled :
124
+ # In the case of a fast sync, we return once the sync is completed, and our
125
+ # caller must then run the StateDownloader.
110
126
return
111
-
112
- # Since self._sync_complete is not set, peer_or_finished can only be a Peer
113
- # instance.
114
- self .run_task (self .sync (peer_or_finished ))
127
+ else :
128
+ self .run_task (self .sync (peer ))
115
129
116
130
async def sync (self , peer : HeaderRequestingPeer ) -> None :
117
131
if self ._syncing :
@@ -162,7 +176,11 @@ async def _sync(self, peer: HeaderRequestingPeer) -> None:
162
176
break
163
177
164
178
try :
165
- headers = await self ._fetch_missing_headers (peer , start_at )
179
+ fetch_headers_coro = self ._fetch_missing_headers (peer , start_at )
180
+ headers = await self .complete_token .cancellable_wait (fetch_headers_coro )
181
+ except OperationCancelled :
182
+ self .logger .info ("Sync with %s completed" , peer )
183
+ break
166
184
except TimeoutError :
167
185
self .logger .warn ("Timeout waiting for header batch from %s, aborting sync" , peer )
168
186
await peer .disconnect (DisconnectReason .timeout )
@@ -192,22 +210,21 @@ async def _sync(self, peer: HeaderRequestingPeer) -> None:
192
210
except ValidationError as e :
193
211
self .logger .warn ("Received invalid headers from %s, aborting sync: %s" , peer , e )
194
212
break
195
- try :
196
- head_number = await self ._process_headers (peer , headers )
197
- except NoEligiblePeers :
198
- self .logger .info ("No peers have the blocks we want, aborting sync" )
199
- break
200
- start_at = head_number + 1
201
213
202
- # Quite often the header batch we receive here includes headers past the peer's reported
203
- # head (via the NewBlock msg), so we can't compare our head's hash to the peer's in
204
- # order to see if the sync is completed. Instead we just check that we have the peer's
205
- # head_hash in our chain.
206
- if await self .wait (self .db .coro_header_exists (peer .head_hash )):
207
- self .logger .info ("Sync with %s completed" , peer )
208
- if self ._exit_on_sync_complete :
209
- self ._sync_complete .set ()
210
- break
214
+ # Setting the latest header hash for the peer, before queuing header processing tasks
215
+ self ._target_header_hash = peer .head_hash
216
+
217
+ await self ._queue_headers_for_processing (headers )
218
+ start_at = headers [- 1 ].block_number + 1
219
+
220
+ async def _queue_headers_for_processing (self , headers : Tuple [BlockHeader , ...]) -> None :
221
+ # this block is an optimization to avoid lots of await calls
222
+ if len (headers ) + self .pending_headers .qsize () <= self .pending_headers .maxsize :
223
+ for header in headers :
224
+ self .pending_headers .put_nowait (header )
225
+ else :
226
+ for header in headers :
227
+ await self .pending_headers .put (header )
211
228
212
229
async def _fetch_missing_headers (
213
230
self , peer : HeaderRequestingPeer , start_at : int ) -> Tuple [BlockHeader , ...]:
@@ -245,12 +262,18 @@ async def get_missing_tail(self: 'BaseHeaderChainSyncer',
245
262
246
263
return tail_headers
247
264
265
+ async def pop_all_pending_headers (self ) -> Tuple [BlockHeader , ...]:
266
+ """Get all the currently pending headers. If no headers pending, wait until one is"""
267
+ queue = self .pending_headers
268
+ if queue .empty ():
269
+ first_header = await queue .get ()
270
+ else :
271
+ first_header = queue .get_nowait ()
272
+
273
+ available = queue .qsize ()
274
+ return (first_header , ) + tuple (queue .get_nowait () for _ in range (available ))
275
+
248
276
@abstractmethod
249
277
async def _handle_msg (self , peer : HeaderRequestingPeer , cmd : protocol .Command ,
250
278
msg : protocol ._DecodedMsgType ) -> None :
251
279
raise NotImplementedError ("Must be implemented by subclasses" )
252
-
253
- @abstractmethod
254
- async def _process_headers (
255
- self , peer : HeaderRequestingPeer , headers : Tuple [BlockHeader , ...]) -> int :
256
- raise NotImplementedError ("Must be implemented by subclasses" )
0 commit comments