Browse Source

rework AddressSynchronizer.is_up_to_date

- AddressSynchronizer no longer has its own state re up_to_date,
  it defers to Synchronizer/Verifier instead
- Synchronizer is now tracking address sync states throughout their lifecycle:
  and Synchronizer.is_up_to_date() checks all states
- Synchronizer.add_queue (internal) is removed as it was redundant
- should fix wallet.is_up_to_date flickering during sync due to race

related:
dc6c481406
1c20a29a22
master
SomberNight 3 years ago
parent
commit
29a0560f98
No known key found for this signature in database
GPG Key ID: B33B5F232C6271E9
  1. 15
      electrum/address_synchronizer.py
  2. 100
      electrum/synchronizer.py

15
electrum/address_synchronizer.py

@ -84,8 +84,6 @@ class AddressSynchronizer(Logger, EventListener):
self.unverified_tx = defaultdict(int) # type: Dict[str, int] # txid -> height. Access with self.lock. self.unverified_tx = defaultdict(int) # type: Dict[str, int] # txid -> height. Access with self.lock.
# Txs the server claims are in the mempool: # Txs the server claims are in the mempool:
self.unconfirmed_tx = defaultdict(int) # type: Dict[str, int] # txid -> height. Access with self.lock. self.unconfirmed_tx = defaultdict(int) # type: Dict[str, int] # txid -> height. Access with self.lock.
# true when synchronized
self._up_to_date = False # considers both Synchronizer and Verifier
# thread local storage for caching stuff # thread local storage for caching stuff
self.threadlocal_cache = threading.local() self.threadlocal_cache = threading.local()
@ -210,9 +208,9 @@ class AddressSynchronizer(Logger, EventListener):
def add_address(self, address): def add_address(self, address):
if address not in self.db.history: if address not in self.db.history:
self.db.history[address] = [] self.db.history[address] = []
self.set_up_to_date(False)
if self.synchronizer: if self.synchronizer:
self.synchronizer.add(address) self.synchronizer.add(address)
self.up_to_date_changed()
def get_conflicting_transactions(self, tx_hash, tx: Transaction, include_self=False): def get_conflicting_transactions(self, tx_hash, tx: Transaction, include_self=False):
"""Returns a set of transaction hashes from the wallet history that are """Returns a set of transaction hashes from the wallet history that are
@ -677,17 +675,14 @@ class AddressSynchronizer(Logger, EventListener):
# local transaction # local transaction
return TxMinedInfo(height=TX_HEIGHT_LOCAL, conf=0) return TxMinedInfo(height=TX_HEIGHT_LOCAL, conf=0)
def set_up_to_date(self, up_to_date): def up_to_date_changed(self) -> None:
with self.lock:
status_changed = self._up_to_date != up_to_date
self._up_to_date = up_to_date
# fire triggers # fire triggers
util.trigger_callback('adb_set_up_to_date', self) util.trigger_callback('adb_set_up_to_date', self)
if status_changed:
self.logger.info(f'set_up_to_date: {up_to_date}')
def is_up_to_date(self): def is_up_to_date(self):
return self._up_to_date if not self.synchronizer or not self.verifier:
return False
return self.synchronizer.is_up_to_date() and self.verifier.is_up_to_date()
def reset_netrequest_counters(self) -> None: def reset_netrequest_counters(self) -> None:
if self.synchronizer: if self.synchronizer:

100
electrum/synchronizer.py

@ -65,18 +65,18 @@ class SynchronizerBase(NetworkJobOnDefaultServer):
def _reset(self): def _reset(self):
super()._reset() super()._reset()
self._adding_addrs = set()
self.requested_addrs = set() self.requested_addrs = set()
self._handling_addr_statuses = set()
self.scripthash_to_address = {} self.scripthash_to_address = {}
self._processed_some_notifications = False # so that we don't miss them self._processed_some_notifications = False # so that we don't miss them
# Queues # Queues
self.add_queue = asyncio.Queue()
self.status_queue = asyncio.Queue() self.status_queue = asyncio.Queue()
async def _run_tasks(self, *, taskgroup): async def _run_tasks(self, *, taskgroup):
await super()._run_tasks(taskgroup=taskgroup) await super()._run_tasks(taskgroup=taskgroup)
try: try:
async with taskgroup as group: async with taskgroup as group:
await group.spawn(self.send_subscriptions())
await group.spawn(self.handle_status()) await group.spawn(self.handle_status())
await group.spawn(self.main()) await group.spawn(self.main())
finally: finally:
@ -84,43 +84,44 @@ class SynchronizerBase(NetworkJobOnDefaultServer):
self.session.unsubscribe(self.status_queue) self.session.unsubscribe(self.status_queue)
def add(self, addr): def add(self, addr):
# FIXME is_up_to_date does not take addr into account until _add_address executes if not is_address(addr): raise ValueError(f"invalid bitcoin address {addr}")
self._adding_addrs.add(addr) # this lets is_up_to_date already know about addr
asyncio.run_coroutine_threadsafe(self._add_address(addr), self.asyncio_loop) asyncio.run_coroutine_threadsafe(self._add_address(addr), self.asyncio_loop)
async def _add_address(self, addr: str): async def _add_address(self, addr: str):
# note: this method is async as add_queue.put_nowait is not thread-safe. try:
if not is_address(addr): raise ValueError(f"invalid bitcoin address {addr}") if not is_address(addr): raise ValueError(f"invalid bitcoin address {addr}")
if addr in self.requested_addrs: return if addr in self.requested_addrs: return
self.requested_addrs.add(addr) self.requested_addrs.add(addr)
self.add_queue.put_nowait(addr) await self.taskgroup.spawn(self._subscribe_to_address, addr)
finally:
self._adding_addrs.discard(addr) # ok for addr not to be present
async def _on_address_status(self, addr, status): async def _on_address_status(self, addr, status):
"""Handle the change of the status of an address.""" """Handle the change of the status of an address.
Should remove addr from self._handling_addr_statuses when done.
"""
raise NotImplementedError() # implemented by subclasses raise NotImplementedError() # implemented by subclasses
async def send_subscriptions(self): async def _subscribe_to_address(self, addr):
async def subscribe_to_address(addr): h = address_to_scripthash(addr)
h = address_to_scripthash(addr) self.scripthash_to_address[h] = addr
self.scripthash_to_address[h] = addr self._requests_sent += 1
self._requests_sent += 1 try:
try: async with self._network_request_semaphore:
async with self._network_request_semaphore: await self.session.subscribe('blockchain.scripthash.subscribe', [h], self.status_queue)
await self.session.subscribe('blockchain.scripthash.subscribe', [h], self.status_queue) except RPCError as e:
except RPCError as e: if e.message == 'history too large': # no unique error code
if e.message == 'history too large': # no unique error code raise GracefulDisconnect(e, log_level=logging.ERROR) from e
raise GracefulDisconnect(e, log_level=logging.ERROR) from e raise
raise self._requests_answered += 1
self._requests_answered += 1
self.requested_addrs.remove(addr)
while True:
addr = await self.add_queue.get()
await self.taskgroup.spawn(subscribe_to_address, addr)
async def handle_status(self): async def handle_status(self):
while True: while True:
h, status = await self.status_queue.get() h, status = await self.status_queue.get()
addr = self.scripthash_to_address[h] addr = self.scripthash_to_address[h]
self._handling_addr_statuses.add(addr)
self.requested_addrs.discard(addr) # ok for addr not to be present
await self.taskgroup.spawn(self._on_address_status, addr, status) await self.taskgroup.spawn(self._on_address_status, addr, status)
self._processed_some_notifications = True self._processed_some_notifications = True
@ -142,6 +143,7 @@ class Synchronizer(SynchronizerBase):
def _reset(self): def _reset(self):
super()._reset() super()._reset()
self._init_done = False
self.requested_tx = {} self.requested_tx = {}
self.requested_histories = set() self.requested_histories = set()
self._stale_histories = dict() # type: Dict[str, asyncio.Task] self._stale_histories = dict() # type: Dict[str, asyncio.Task]
@ -150,22 +152,29 @@ class Synchronizer(SynchronizerBase):
return self.adb.diagnostic_name() return self.adb.diagnostic_name()
def is_up_to_date(self): def is_up_to_date(self):
return (not self.requested_addrs return (self._init_done
and not self._adding_addrs
and not self.requested_addrs
and not self._handling_addr_statuses
and not self.requested_histories and not self.requested_histories
and not self.requested_tx and not self.requested_tx
and not self._stale_histories) and not self._stale_histories
and self.status_queue.empty())
async def _on_address_status(self, addr, status): async def _on_address_status(self, addr, status):
history = self.adb.db.get_addr_history(addr) try:
if history_status(history) == status: history = self.adb.db.get_addr_history(addr)
return if history_status(history) == status:
# No point in requesting history twice for the same announced status. return
# However if we got announced a new status, we should request history again: # No point in requesting history twice for the same announced status.
if (addr, status) in self.requested_histories: # However if we got announced a new status, we should request history again:
return if (addr, status) in self.requested_histories:
# request address history return
self.requested_histories.add((addr, status)) # request address history
self._stale_histories.pop(addr, asyncio.Future()).cancel() self.requested_histories.add((addr, status))
self._stale_histories.pop(addr, asyncio.Future()).cancel()
finally:
self._handling_addr_statuses.discard(addr)
h = address_to_scripthash(addr) h = address_to_scripthash(addr)
self._requests_sent += 1 self._requests_sent += 1
async with self._network_request_semaphore: async with self._network_request_semaphore:
@ -236,7 +245,7 @@ class Synchronizer(SynchronizerBase):
self.logger.info(f"received tx {tx_hash} height: {tx_height} bytes: {len(raw_tx)}") self.logger.info(f"received tx {tx_hash} height: {tx_height} bytes: {len(raw_tx)}")
async def main(self): async def main(self):
self.adb.set_up_to_date(False) self.adb.up_to_date_changed()
# request missing txns, if any # request missing txns, if any
for addr in random_shuffled_copy(self.adb.db.get_history()): for addr in random_shuffled_copy(self.adb.db.get_history()):
history = self.adb.db.get_addr_history(addr) history = self.adb.db.get_addr_history(addr)
@ -248,16 +257,17 @@ class Synchronizer(SynchronizerBase):
for addr in random_shuffled_copy(self.adb.get_addresses()): for addr in random_shuffled_copy(self.adb.get_addresses()):
await self._add_address(addr) await self._add_address(addr)
# main loop # main loop
self._init_done = True
prev_uptodate = False
while True: while True:
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
hist_done = self.is_up_to_date() up_to_date = self.adb.is_up_to_date()
spv_done = self.adb.verifier.is_up_to_date() if self.adb.verifier else True
up_to_date = hist_done and spv_done
# see if status changed # see if status changed
if (up_to_date != self.adb.is_up_to_date() if (up_to_date != prev_uptodate
or up_to_date and self._processed_some_notifications): or up_to_date and self._processed_some_notifications):
self._processed_some_notifications = False self._processed_some_notifications = False
self.adb.set_up_to_date(up_to_date) self.adb.up_to_date_changed()
prev_uptodate = up_to_date
class Notifier(SynchronizerBase): class Notifier(SynchronizerBase):

Loading…
Cancel
Save