Clean up controller init, and sync on catch up

This commit is contained in:
Neil Booth 2016-10-17 20:20:10 +09:00
parent 208ed5d2b2
commit 1084060493
2 changed files with 57 additions and 56 deletions

View File

@ -54,23 +54,28 @@ class Controller(LoggedClass):
.format(env.host, env.ssl_port)) .format(env.host, env.ssl_port))
coros = [ coros = [
self.block_cache.catch_up(), self.block_cache.prefetcher(),
self.block_cache.process_cache() self.block_cache.process_blocks(),
] ]
self.tasks = [asyncio.ensure_future(coro) for coro in coros] for coro in coros:
asyncio.ensure_future(coro)
# Signal handlers # Signal handlers
for signame in ('SIGINT', 'SIGTERM'): for signame in ('SIGINT', 'SIGTERM'):
loop.add_signal_handler(getattr(signal, signame), loop.add_signal_handler(getattr(signal, signame),
partial(self.on_signal, signame)) partial(self.on_signal, loop, signame))
return self.tasks
def stop(self): def stop(self):
for server in self.servers: for server in self.servers:
server.close() server.close()
def on_signal(self, loop, signame):
self.logger.warning('received {} signal, preparing to shut down'
.format(signame))
for task in asyncio.Task.all_tasks(loop):
task.cancel()
def add_session(self, session): def add_session(self, session):
self.sessions.add(session) self.sessions.add(session)
@ -97,12 +102,6 @@ class Controller(LoggedClass):
self.jobs = jobs self.jobs = jobs
await asyncio.sleep(5) await asyncio.sleep(5)
def on_signal(self, signame):
self.logger.warning('received {} signal, preparing to shut down'
.format(signame))
for task in self.tasks:
task.cancel()
def address_status(self, hash168): def address_status(self, hash168):
'''Returns status as 32 bytes.''' '''Returns status as 32 bytes.'''
status = self.addresses.get(hash168) status = self.addresses.get(hash168)
@ -122,70 +121,70 @@ class Controller(LoggedClass):
per peer.''' per peer.'''
return self.peers return self.peers
class BlockCache(LoggedClass): class BlockCache(LoggedClass):
'''Requests blocks ahead of time from the daemon. Serves them '''Requests and caches blocks ahead of time from the daemon. Serves
to the blockchain processor.''' them to the blockchain processor. Coordinates backing up in case of
block chain reorganisations.
'''
def __init__(self, env, db): def __init__(self, env, db):
super().__init__() super().__init__()
self.db = db self.db = db
self.daemon_url = env.daemon_url self.daemon_url = env.daemon_url
# Cache target size is in MB. Has little effect on sync time. # Target cache size. Has little effect on sync time.
self.cache_limit = 10 self.target_cache_size = 10 * 1024 * 1024
self.daemon_height = 0 self.daemon_height = 0
self.fetched_height = db.height self.fetched_height = db.height
# Blocks stored in reverse order. Next block is at end of list. self.queue = asyncio.Queue()
self.blocks = [] self.queue_size = 0
self.recent_sizes = [] self.recent_sizes = [0]
self.ave_size = 0
self.logger.info('using daemon URL {}'.format(self.daemon_url)) self.logger.info('using daemon URL {}'.format(self.daemon_url))
async def process_cache(self): def flush_db(self):
while True: self.db.flush(self.daemon_height, True)
await asyncio.sleep(1)
while self.blocks:
self.db.process_block(self.blocks.pop(), self.daemon_height)
# Release asynchronous block fetching
await asyncio.sleep(0)
async def catch_up(self):
self.logger.info('catching up, block cache limit {:d}MB...'
.format(self.cache_limit))
async def process_blocks(self):
try: try:
while await self.maybe_prefill(): while True:
await asyncio.sleep(1) blocks, total_size = await self.queue.get()
self.logger.info('caught up to height {:d}' self.queue_size -= total_size
.format(self.daemon_height)) for block in blocks:
self.db.process_block(block, self.daemon_height)
# Release asynchronous block fetching
await asyncio.sleep(0)
if self.db.height == self.daemon_height:
self.logger.info('caught up to height {:d}'
.format(self.daemon_height))
self.flush_db()
finally: finally:
self.db.flush(self.daemon_height, True) self.flush_db()
async def prefetcher(self):
'''Loops forever polling for more blocks.'''
self.logger.info('prefetching blocks...')
while True:
await self.maybe_prefetch()
await asyncio.sleep(2)
def cache_used(self): def cache_used(self):
return sum(len(block) for block in self.blocks) return sum(len(block) for block in self.blocks)
def prefill_count(self, room): def prefill_count(self, room):
count = 0 ave_size = sum(self.recent_sizes) // len(self.recent_sizes)
if self.ave_size: count = room // ave_size if ave_size else 0
count = room // self.ave_size
return max(count, 10) return max(count, 10)
async def maybe_prefill(self): async def maybe_prefetch(self):
'''Returns False to stop. True to sleep a while for asynchronous '''Prefetch blocks if there are any to prefetch.'''
processing.''' while self.queue_size < self.target_cache_size:
cache_limit = self.cache_limit * 1024 * 1024
while True:
cache_used = self.cache_used()
if cache_used > cache_limit:
return True
# Keep going by getting a whole new cache_limit of blocks # Keep going by getting a whole new cache_limit of blocks
self.daemon_height = await self.send_single('getblockcount') self.daemon_height = await self.send_single('getblockcount')
max_count = min(self.daemon_height - self.fetched_height, 4000) max_count = min(self.daemon_height - self.fetched_height, 4000)
count = min(max_count, self.prefill_count(cache_limit)) count = min(max_count, self.prefill_count(self.target_cache_size))
if not count: if not count:
return False # Done catching up break
first = self.fetched_height + 1 first = self.fetched_height + 1
param_lists = [[height] for height in range(first, first + count)] param_lists = [[height] for height in range(first, first + count)]
@ -198,16 +197,16 @@ class BlockCache(LoggedClass):
# Convert hex string to bytes # Convert hex string to bytes
blocks = [bytes.fromhex(block) for block in blocks] blocks = [bytes.fromhex(block) for block in blocks]
# Reverse order and place at front of list sizes = [len(block) for block in blocks]
self.blocks = list(reversed(blocks)) + self.blocks total_size = sum(sizes)
self.queue.put_nowait((blocks, total_size))
self.queue_size += total_size
# Keep 50 most recent block sizes for fetch count estimation # Keep 50 most recent block sizes for fetch count estimation
sizes = [len(block) for block in blocks]
self.recent_sizes.extend(sizes) self.recent_sizes.extend(sizes)
excess = len(self.recent_sizes) - 50 excess = len(self.recent_sizes) - 50
if excess > 0: if excess > 0:
self.recent_sizes = self.recent_sizes[excess:] self.recent_sizes = self.recent_sizes[excess:]
self.ave_size = sum(self.recent_sizes) // len(self.recent_sizes)
async def send_single(self, method, params=None): async def send_single(self, method, params=None):
payload = {'method': method} payload = {'method': method}

View File

@ -26,7 +26,9 @@ def main_loop():
#loop.set_debug(True) #loop.set_debug(True)
controller = Controller(env) controller = Controller(env)
tasks = controller.start(loop) controller.start(loop)
tasks = asyncio.Task.all_tasks(loop)
try: try:
loop.run_until_complete(asyncio.gather(*tasks)) loop.run_until_complete(asyncio.gather(*tasks))
except asyncio.CancelledError: except asyncio.CancelledError: