Rework mempool and BP<->prefretcher communication
This commit is contained in:
parent
431989f0ea
commit
ca3ba2b2d8
@ -23,12 +23,17 @@ from electrumx.lib.util import chunks, formatted_time, class_logger
|
|||||||
import electrumx.server.db
|
import electrumx.server.db
|
||||||
|
|
||||||
|
|
||||||
|
RAW_BLOCKS, PREFETCHER_CAUGHT_UP, REORG_CHAIN = range(3)
|
||||||
|
|
||||||
|
|
||||||
class Prefetcher(object):
|
class Prefetcher(object):
|
||||||
'''Prefetches blocks (in the forward direction only).'''
|
'''Prefetches blocks (in the forward direction only).'''
|
||||||
|
|
||||||
def __init__(self, bp):
|
def __init__(self, daemon, coin, queue):
|
||||||
self.logger = class_logger(__name__, self.__class__.__name__)
|
self.logger = class_logger(__name__, self.__class__.__name__)
|
||||||
self.bp = bp
|
self.daemon = daemon
|
||||||
|
self.coin = coin
|
||||||
|
self.queue = queue
|
||||||
self.caught_up = False
|
self.caught_up = False
|
||||||
# Access to fetched_height should be protected by the semaphore
|
# Access to fetched_height should be protected by the semaphore
|
||||||
self.fetched_height = None
|
self.fetched_height = None
|
||||||
@ -58,18 +63,19 @@ class Prefetcher(object):
|
|||||||
if self.cache_size < self.min_cache_size:
|
if self.cache_size < self.min_cache_size:
|
||||||
self.refill_event.set()
|
self.refill_event.set()
|
||||||
|
|
||||||
async def reset_height(self):
|
async def reset_height(self, height):
|
||||||
'''Reset to prefetch blocks from the block processor's height.
|
'''Reset to prefetch blocks from the block processor's height.
|
||||||
|
|
||||||
Used in blockchain reorganisations. This coroutine can be
|
Used in blockchain reorganisations. This coroutine can be
|
||||||
called asynchronously to the _prefetch coroutine so we must
|
called asynchronously to the _prefetch_blocks coroutine so we
|
||||||
synchronize with a semaphore.'''
|
must synchronize with a semaphore.
|
||||||
|
'''
|
||||||
async with self.semaphore:
|
async with self.semaphore:
|
||||||
self.fetched_height = self.bp.height
|
self.fetched_height = height
|
||||||
self.refill_event.set()
|
self.refill_event.set()
|
||||||
|
|
||||||
daemon_height = await self.bp.daemon.height()
|
daemon_height = await self.daemon.height()
|
||||||
behind = daemon_height - self.bp.height
|
behind = daemon_height - height
|
||||||
if behind > 0:
|
if behind > 0:
|
||||||
self.logger.info('catching up to daemon height {:,d} '
|
self.logger.info('catching up to daemon height {:,d} '
|
||||||
'({:,d} blocks behind)'
|
'({:,d} blocks behind)'
|
||||||
@ -83,8 +89,8 @@ class Prefetcher(object):
|
|||||||
|
|
||||||
Repeats until the queue is full or caught up.
|
Repeats until the queue is full or caught up.
|
||||||
'''
|
'''
|
||||||
daemon = self.bp.daemon
|
daemon = self.daemon
|
||||||
daemon_height = await daemon.height(self.bp._caught_up_event.is_set())
|
daemon_height = await daemon.height()
|
||||||
async with self.semaphore:
|
async with self.semaphore:
|
||||||
while self.cache_size < self.min_cache_size:
|
while self.cache_size < self.min_cache_size:
|
||||||
# Try and catch up all blocks but limit to room in cache.
|
# Try and catch up all blocks but limit to room in cache.
|
||||||
@ -96,7 +102,7 @@ class Prefetcher(object):
|
|||||||
if not count:
|
if not count:
|
||||||
if not self.caught_up:
|
if not self.caught_up:
|
||||||
self.caught_up = True
|
self.caught_up = True
|
||||||
self.bp.on_prefetcher_first_caught_up()
|
await self.queue.put((PREFETCHER_CAUGHT_UP, ))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
first = self.fetched_height + 1
|
first = self.fetched_height + 1
|
||||||
@ -110,7 +116,7 @@ class Prefetcher(object):
|
|||||||
|
|
||||||
# Special handling for genesis block
|
# Special handling for genesis block
|
||||||
if first == 0:
|
if first == 0:
|
||||||
blocks[0] = self.bp.coin.genesis_block(blocks[0])
|
blocks[0] = self.coin.genesis_block(blocks[0])
|
||||||
self.logger.info('verified genesis block with hash {}'
|
self.logger.info('verified genesis block with hash {}'
|
||||||
.format(hex_hashes[0]))
|
.format(hex_hashes[0]))
|
||||||
|
|
||||||
@ -121,7 +127,7 @@ class Prefetcher(object):
|
|||||||
else:
|
else:
|
||||||
self.ave_size = (size + (10 - count) * self.ave_size) // 10
|
self.ave_size = (size + (10 - count) * self.ave_size) // 10
|
||||||
|
|
||||||
self.bp.on_prefetched_blocks(blocks, first)
|
await self.queue.put((RAW_BLOCKS, blocks, first))
|
||||||
self.cache_size += size
|
self.cache_size += size
|
||||||
self.fetched_height += count
|
self.fetched_height += count
|
||||||
|
|
||||||
@ -152,9 +158,10 @@ class BlockProcessor(electrumx.server.db.DB):
|
|||||||
self.tasks = tasks
|
self.tasks = tasks
|
||||||
self.daemon = daemon
|
self.daemon = daemon
|
||||||
|
|
||||||
|
# Work queue
|
||||||
|
self.queue = asyncio.Queue()
|
||||||
self._caught_up_event = asyncio.Event()
|
self._caught_up_event = asyncio.Event()
|
||||||
self.task_queue = asyncio.Queue()
|
self.prefetcher = Prefetcher(daemon, env.coin, self.queue)
|
||||||
self.prefetcher = Prefetcher(self)
|
|
||||||
|
|
||||||
# Meta
|
# Meta
|
||||||
self.cache_MB = env.cache_MB
|
self.cache_MB = env.cache_MB
|
||||||
@ -185,17 +192,6 @@ class BlockProcessor(electrumx.server.db.DB):
|
|||||||
'''Add the task to our task queue.'''
|
'''Add the task to our task queue.'''
|
||||||
self.task_queue.put_nowait(task)
|
self.task_queue.put_nowait(task)
|
||||||
|
|
||||||
def on_prefetched_blocks(self, blocks, first):
|
|
||||||
'''Called by the prefetcher when it has prefetched some blocks.'''
|
|
||||||
self.add_task(partial(self.check_and_advance_blocks, blocks, first))
|
|
||||||
|
|
||||||
def on_prefetcher_first_caught_up(self):
|
|
||||||
'''Called by the prefetcher when it first catches up.'''
|
|
||||||
# Process after prior tasks (blocks) are completed.
|
|
||||||
async def set_event():
|
|
||||||
self._caught_up_event.set()
|
|
||||||
self.add_task(set_event)
|
|
||||||
|
|
||||||
def add_new_block_callback(self, callback):
|
def add_new_block_callback(self, callback):
|
||||||
'''Add a function called when a new block is found.
|
'''Add a function called when a new block is found.
|
||||||
|
|
||||||
@ -247,7 +243,7 @@ class BlockProcessor(electrumx.server.db.DB):
|
|||||||
# just to reset the prefetcher and try again.
|
# just to reset the prefetcher and try again.
|
||||||
self.logger.warning('daemon blocks do not form a chain; '
|
self.logger.warning('daemon blocks do not form a chain; '
|
||||||
'resetting the prefetcher')
|
'resetting the prefetcher')
|
||||||
await self.prefetcher.reset_height()
|
await self.prefetcher.reset_height(self.height)
|
||||||
|
|
||||||
async def reorg_chain(self, count=None):
|
async def reorg_chain(self, count=None):
|
||||||
'''Handle a chain reorganisation.
|
'''Handle a chain reorganisation.
|
||||||
@ -280,7 +276,7 @@ class BlockProcessor(electrumx.server.db.DB):
|
|||||||
last -= len(raw_blocks)
|
last -= len(raw_blocks)
|
||||||
# Truncate header_mc: header count is 1 more than the height
|
# Truncate header_mc: header count is 1 more than the height
|
||||||
self.header_mc.truncate(self.height + 1)
|
self.header_mc.truncate(self.height + 1)
|
||||||
await self.prefetcher.reset_height()
|
await self.prefetcher.reset_height(self.height)
|
||||||
|
|
||||||
async def reorg_hashes(self, count):
|
async def reorg_hashes(self, count):
|
||||||
'''Return a pair (start, hashes) of blocks to back up during a
|
'''Return a pair (start, hashes) of blocks to back up during a
|
||||||
@ -760,8 +756,15 @@ class BlockProcessor(electrumx.server.db.DB):
|
|||||||
async def _process_queue(self):
|
async def _process_queue(self):
|
||||||
'''Loop forever processing enqueued work.'''
|
'''Loop forever processing enqueued work.'''
|
||||||
while True:
|
while True:
|
||||||
task = await self.task_queue.get()
|
work, *args = await self.queue.get()
|
||||||
await task()
|
if work == RAW_BLOCKS:
|
||||||
|
raw_blocks, first = args
|
||||||
|
await self.check_and_advance_blocks(raw_blocks, first)
|
||||||
|
elif work == PREFETCHER_CAUGHT_UP:
|
||||||
|
self._caught_up_event.set()
|
||||||
|
elif work == REORG_CHAIN:
|
||||||
|
count, = args
|
||||||
|
await self.reorg_chain(count)
|
||||||
|
|
||||||
def _on_dbs_opened(self):
|
def _on_dbs_opened(self):
|
||||||
# An incomplete compaction needs to be cancelled otherwise
|
# An incomplete compaction needs to be cancelled otherwise
|
||||||
@ -790,7 +793,7 @@ class BlockProcessor(electrumx.server.db.DB):
|
|||||||
self._on_dbs_opened()
|
self._on_dbs_opened()
|
||||||
# Get the prefetcher running
|
# Get the prefetcher running
|
||||||
self.tasks.create_task(self.prefetcher.main_loop())
|
self.tasks.create_task(self.prefetcher.main_loop())
|
||||||
await self.prefetcher.reset_height()
|
await self.prefetcher.reset_height(self.height)
|
||||||
# Start our loop that processes blocks as they are fetched
|
# Start our loop that processes blocks as they are fetched
|
||||||
self.worker_task = self.tasks.create_task(self._process_queue())
|
self.worker_task = self.tasks.create_task(self._process_queue())
|
||||||
# Wait until caught up
|
# Wait until caught up
|
||||||
@ -816,7 +819,7 @@ class BlockProcessor(electrumx.server.db.DB):
|
|||||||
Returns True if a reorg is queued, false if not caught up.
|
Returns True if a reorg is queued, false if not caught up.
|
||||||
'''
|
'''
|
||||||
if self._caught_up_event.is_set():
|
if self._caught_up_event.is_set():
|
||||||
self.add_task(partial(self.reorg_chain, count=count))
|
self.queue.put_nowait((REORG_CHAIN, count))
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
# and warranty status of this software.
|
# and warranty status of this software.
|
||||||
|
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import pylru
|
import pylru
|
||||||
|
|
||||||
from electrumx.server.mempool import MemPool
|
from electrumx.server.mempool import MemPool
|
||||||
@ -108,5 +109,9 @@ class ChainState(object):
|
|||||||
|
|
||||||
async def wait_for_mempool(self):
|
async def wait_for_mempool(self):
|
||||||
await self.bp.catch_up_to_daemon()
|
await self.bp.catch_up_to_daemon()
|
||||||
self.tasks.create_task(self.mempool.main_loop())
|
# Tell the daemon to fetch the mempool going forwards, trigger
|
||||||
await self.mempool.synchronized_event.wait()
|
# an initial fetch, and wait for the mempool to synchronize
|
||||||
|
mempool_refresh_event = asyncio.Event()
|
||||||
|
daemon._mempool_refresh_event = mempool_refresh_event
|
||||||
|
self.tasks.create_task(self.daemon.height())
|
||||||
|
await self.mempool.start_and_wait(mempool_refresh_event)
|
||||||
|
|||||||
@ -41,7 +41,7 @@ class Daemon(object):
|
|||||||
self.set_urls(env.coin.daemon_urls(env.daemon_url))
|
self.set_urls(env.coin.daemon_urls(env.daemon_url))
|
||||||
self._height = None
|
self._height = None
|
||||||
self._mempool_hashes = set()
|
self._mempool_hashes = set()
|
||||||
self.mempool_refresh_event = asyncio.Event()
|
self._mempool_refresh_event = None
|
||||||
# Limit concurrent RPC calls to this number.
|
# Limit concurrent RPC calls to this number.
|
||||||
# See DEFAULT_HTTP_WORKQUEUE in bitcoind, which is typically 16
|
# See DEFAULT_HTTP_WORKQUEUE in bitcoind, which is typically 16
|
||||||
self.workqueue_semaphore = asyncio.Semaphore(value=10)
|
self.workqueue_semaphore = asyncio.Semaphore(value=10)
|
||||||
@ -281,12 +281,12 @@ class Daemon(object):
|
|||||||
'''Broadcast a transaction to the network.'''
|
'''Broadcast a transaction to the network.'''
|
||||||
return await self._send_single('sendrawtransaction', params)
|
return await self._send_single('sendrawtransaction', params)
|
||||||
|
|
||||||
async def height(self, mempool=False):
|
async def height(self):
|
||||||
'''Query the daemon for its current height.'''
|
'''Query the daemon for its current height.'''
|
||||||
self._height = await self._send_single('getblockcount')
|
self._height = await self._send_single('getblockcount')
|
||||||
if mempool:
|
if self._mempool_refresh_event:
|
||||||
self._mempool_hashes = set(await self.mempool_hashes())
|
self._mempool_hashes = set(await self.mempool_hashes())
|
||||||
self.mempool_refresh_event.set()
|
self._mempool_refresh_event.set()
|
||||||
return self._height
|
return self._height
|
||||||
|
|
||||||
def cached_mempool_hashes(self):
|
def cached_mempool_hashes(self):
|
||||||
|
|||||||
@ -41,12 +41,21 @@ class MemPool(object):
|
|||||||
self.stop = False
|
self.stop = False
|
||||||
self.txs = {}
|
self.txs = {}
|
||||||
self.hashXs = defaultdict(set) # None can be a key
|
self.hashXs = defaultdict(set) # None can be a key
|
||||||
self.synchronized_event = asyncio.Event()
|
|
||||||
self.fee_histogram = defaultdict(int)
|
self.fee_histogram = defaultdict(int)
|
||||||
self.compact_fee_histogram = []
|
self.compact_fee_histogram = []
|
||||||
self.histogram_time = 0
|
self.histogram_time = 0
|
||||||
add_new_block_callback(self.on_new_block)
|
add_new_block_callback(self.on_new_block)
|
||||||
|
|
||||||
|
async def start_and_wait(self, mempool_refresh_event):
|
||||||
|
'''Creates the mempool synchronization task, and waits for it to
|
||||||
|
first synchronize before returning.'''
|
||||||
|
self.logger.info('beginning processing of daemon mempool. '
|
||||||
|
'This can take some time...')
|
||||||
|
synchronized = asyncio.Event()
|
||||||
|
self.tasks.create_task(self._synchronize(
|
||||||
|
mempool_refresh_event, synchronized))
|
||||||
|
await synchronized.wait()
|
||||||
|
|
||||||
def _resync_daemon_hashes(self, unprocessed, unfetched):
|
def _resync_daemon_hashes(self, unprocessed, unfetched):
|
||||||
'''Re-sync self.txs with the list of hashes in the daemon's mempool.
|
'''Re-sync self.txs with the list of hashes in the daemon's mempool.
|
||||||
|
|
||||||
@ -83,21 +92,17 @@ class MemPool(object):
|
|||||||
for hex_hash in new:
|
for hex_hash in new:
|
||||||
txs[hex_hash] = None
|
txs[hex_hash] = None
|
||||||
|
|
||||||
async def main_loop(self):
|
async def _synchronize(self, mempool_refresh_event, synchronized):
|
||||||
'''Asynchronously maintain mempool status with daemon.
|
'''Asynchronously maintain mempool status with daemon.
|
||||||
|
|
||||||
Processes the mempool each time the daemon's mempool refresh
|
Processes the mempool each time the mempool refresh event is
|
||||||
event is signalled.
|
signalled.
|
||||||
'''
|
'''
|
||||||
unprocessed = {}
|
unprocessed = {}
|
||||||
unfetched = set()
|
unfetched = set()
|
||||||
txs = self.txs
|
txs = self.txs
|
||||||
fetch_size = 800
|
fetch_size = 800
|
||||||
process_some = self._async_process_some(fetch_size // 2)
|
process_some = self._async_process_some(fetch_size // 2)
|
||||||
|
|
||||||
self.logger.info('beginning processing of daemon mempool. '
|
|
||||||
'This can take some time...')
|
|
||||||
await self.chain_state.mempool_refresh_event.wait()
|
|
||||||
next_log = 0
|
next_log = 0
|
||||||
loops = -1 # Zero during initial catchup
|
loops = -1 # Zero during initial catchup
|
||||||
|
|
||||||
@ -116,7 +121,7 @@ class MemPool(object):
|
|||||||
if not todo:
|
if not todo:
|
||||||
loops += 1
|
loops += 1
|
||||||
if loops > 0:
|
if loops > 0:
|
||||||
self.synchronized_event.set()
|
synchronized.set()
|
||||||
now = time.time()
|
now = time.time()
|
||||||
if now >= next_log and loops:
|
if now >= next_log and loops:
|
||||||
self.logger.info('{:,d} txs touching {:,d} addresses'
|
self.logger.info('{:,d} txs touching {:,d} addresses'
|
||||||
@ -125,10 +130,10 @@ class MemPool(object):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
if not todo:
|
if not todo:
|
||||||
await self.chain_state.mempool_refresh_event.wait()
|
await mempool_refresh_event.wait()
|
||||||
|
|
||||||
self._resync_daemon_hashes(unprocessed, unfetched)
|
self._resync_daemon_hashes(unprocessed, unfetched)
|
||||||
self.chain_state.mempool_refresh_event.clear()
|
mempool_refresh_event.clear()
|
||||||
|
|
||||||
if unfetched:
|
if unfetched:
|
||||||
count = min(len(unfetched), fetch_size)
|
count = min(len(unfetched), fetch_size)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user