lbry-sdk/torba/client/basenetwork.py

229 lines
8.3 KiB
Python
Raw Normal View History

2018-05-25 08:03:25 +02:00
import logging
import asyncio
2018-10-17 19:32:45 +02:00
from asyncio import CancelledError
2018-12-11 05:38:15 +01:00
from time import time
2018-10-15 04:16:51 +02:00
2018-12-05 07:01:11 +01:00
from torba.rpc import RPCSession as BaseClientSession, Connector, RPCError
2018-05-25 08:03:25 +02:00
from torba import __version__
from torba.stream import StreamController
2018-07-01 23:20:17 +02:00
log = logging.getLogger(__name__)
2018-05-25 08:03:25 +02:00
2018-10-15 04:16:51 +02:00
class ClientSession(BaseClientSession):
2018-05-25 08:03:25 +02:00
2018-10-30 21:26:07 +01:00
def __init__(self, *args, network, server, **kwargs):
2018-05-25 08:03:25 +02:00
self.network = network
2018-10-30 21:26:07 +01:00
self.server = server
2018-10-15 04:16:51 +02:00
super().__init__(*args, **kwargs)
self._on_disconnect_controller = StreamController()
self.on_disconnected = self._on_disconnect_controller.stream
2018-10-17 19:05:47 +02:00
self.bw_limit = self.framer.max_size = self.max_errors = 1 << 32
2018-12-11 05:38:15 +01:00
self.max_seconds_idle = 60
self.ping_task = None
2018-10-15 04:16:51 +02:00
2018-11-25 22:30:55 +01:00
async def send_request(self, method, args=()):
try:
return await super().send_request(method, args)
except RPCError as e:
log.warning("Wallet server returned an error. Code: %s Message: %s", *e.args)
raise e
2018-12-11 05:38:15 +01:00
async def ping_forever(self):
# TODO: change to 'ping' on newer protocol (above 1.2)
while not self.is_closing():
if (time() - self.last_send) > self.max_seconds_idle:
await self.send_request('server.banner')
await asyncio.sleep(self.max_seconds_idle//3)
2019-02-12 22:51:12 +01:00
async def create_connection(self, timeout=6):
2018-10-30 21:26:07 +01:00
connector = Connector(lambda: self, *self.server)
2019-02-12 22:51:12 +01:00
await asyncio.wait_for(connector.create_connection(), timeout=timeout)
2018-12-11 05:38:15 +01:00
self.ping_task = asyncio.create_task(self.ping_forever())
2018-10-30 21:26:07 +01:00
2018-10-15 04:16:51 +02:00
async def handle_request(self, request):
controller = self.network.subscription_controllers[request.method]
controller.add(request.args)
2018-05-25 08:03:25 +02:00
2018-10-15 04:16:51 +02:00
def connection_lost(self, exc):
super().connection_lost(exc)
self._on_disconnect_controller.add(True)
2018-12-11 05:38:15 +01:00
if self.ping_task:
self.ping_task.cancel()
2018-05-25 08:03:25 +02:00
class BaseNetwork:
2018-06-08 05:47:46 +02:00
def __init__(self, ledger):
self.config = ledger.config
2018-10-15 04:16:51 +02:00
self.client: ClientSession = None
2019-06-10 01:29:12 +02:00
self.session_pool: SessionPool = None
2018-05-25 08:03:25 +02:00
self.running = False
self._on_connected_controller = StreamController()
self.on_connected = self._on_connected_controller.stream
self._on_header_controller = StreamController()
self.on_header = self._on_header_controller.stream
self._on_status_controller = StreamController()
self.on_status = self._on_status_controller.stream
self.subscription_controllers = {
'blockchain.headers.subscribe': self._on_header_controller,
'blockchain.address.subscribe': self._on_status_controller,
2018-05-25 08:03:25 +02:00
}
2018-10-15 04:16:51 +02:00
async def start(self):
2018-09-17 20:32:16 +02:00
self.running = True
2019-02-12 22:51:12 +01:00
connect_timeout = self.config.get('connect_timeout', 6)
2019-06-10 01:29:12 +02:00
self.session_pool = SessionPool(network=self, timeout=connect_timeout)
self.session_pool.start(self.config['default_servers'])
2019-06-03 10:21:57 +02:00
while True:
2018-05-25 08:03:25 +02:00
try:
2019-06-10 01:29:12 +02:00
self.client = await self.session_pool.pick_fastest_server()
2019-06-04 07:34:58 +02:00
if self.is_connected:
await self.ensure_server_version()
log.info("Successfully connected to SPV wallet server: %s:%d", *self.client.server)
self._on_connected_controller.add(True)
await self.client.on_disconnected.first
except CancelledError:
self.running = False
2019-02-12 22:51:12 +01:00
except asyncio.TimeoutError:
2019-06-04 07:34:58 +02:00
log.warning("Timed out while trying to find a server!")
except Exception: # pylint: disable=broad-except
2019-06-04 07:34:58 +02:00
log.exception("Exception while trying to find a server!")
2018-05-25 08:03:25 +02:00
if not self.running:
return
elif self.client:
await self.client.close()
self.client.connection.cancel_pending_requests()
2018-05-25 08:03:25 +02:00
2018-10-15 04:16:51 +02:00
async def stop(self):
2018-05-25 08:03:25 +02:00
self.running = False
2019-06-10 01:29:12 +02:00
if self.session_pool:
self.session_pool.stop()
2018-05-25 08:03:25 +02:00
if self.is_connected:
2018-10-15 06:45:21 +02:00
disconnected = self.client.on_disconnected.first
2018-10-15 04:16:51 +02:00
await self.client.close()
2018-10-15 06:45:21 +02:00
await disconnected
2018-05-25 08:03:25 +02:00
@property
def is_connected(self):
2018-10-15 04:16:51 +02:00
return self.client is not None and not self.client.is_closing()
2018-05-25 08:03:25 +02:00
def rpc(self, list_or_method, args):
2018-05-25 08:03:25 +02:00
if self.is_connected:
2018-10-15 04:16:51 +02:00
return self.client.send_request(list_or_method, args)
2018-05-25 08:03:25 +02:00
else:
raise ConnectionError("Attempting to send rpc request when connection is not available.")
def ensure_server_version(self, required='1.2'):
return self.rpc('server.version', [__version__, required])
2018-05-25 08:03:25 +02:00
def broadcast(self, raw_transaction):
return self.rpc('blockchain.transaction.broadcast', [raw_transaction])
2018-05-25 08:03:25 +02:00
def get_history(self, address):
return self.rpc('blockchain.address.get_history', [address])
2018-05-25 08:03:25 +02:00
def get_transaction(self, tx_hash):
return self.rpc('blockchain.transaction.get', [tx_hash])
2018-05-25 08:03:25 +02:00
2019-05-05 23:17:36 +02:00
def get_transaction_height(self, tx_hash):
return self.rpc('blockchain.transaction.get_height', [tx_hash])
2018-05-25 08:03:25 +02:00
def get_merkle(self, tx_hash, height):
return self.rpc('blockchain.transaction.get_merkle', [tx_hash, height])
2018-05-25 08:03:25 +02:00
def get_headers(self, height, count=10000):
return self.rpc('blockchain.block.headers', [height, count])
2018-05-25 08:03:25 +02:00
def subscribe_headers(self):
return self.rpc('blockchain.headers.subscribe', [True])
2018-05-25 08:03:25 +02:00
def subscribe_address(self, address):
return self.rpc('blockchain.address.subscribe', [address])
2019-06-10 01:29:12 +02:00
class SessionPool:
def __init__(self, network: BaseNetwork, timeout: float):
self.network = network
self.sessions = []
self._dead_servers = []
self.maintain_connections_task = None
self.timeout = timeout
# triggered when the master server is out, to speed up reconnect
self._lost_master = asyncio.Event()
@property
def online(self):
for session in self.sessions:
if not session.is_closing():
return True
return False
def start(self, default_servers):
self.sessions = [
ClientSession(network=self.network, server=server)
for server in default_servers
]
self.maintain_connections_task = asyncio.create_task(self.ensure_connections())
def stop(self):
if self.maintain_connections_task:
self.maintain_connections_task.cancel()
for session in self.sessions:
if not session.is_closing():
session.abort()
self.sessions, self._dead_servers, self.maintain_connections_task = [], [], None
async def ensure_connections(self):
while True:
await asyncio.gather(*[
self.ensure_connection(session)
for session in self.sessions
], return_exceptions=True)
await asyncio.wait([asyncio.sleep(3), self._lost_master.wait()], return_when='FIRST_COMPLETED')
self._lost_master.clear()
if not self.sessions:
self.sessions.extend(self._dead_servers)
self._dead_servers = []
async def ensure_connection(self, session):
if not session.is_closing():
return
try:
return await session.create_connection(self.timeout)
except asyncio.TimeoutError:
log.warning("Timeout connecting to %s:%d", *session.server)
except asyncio.CancelledError: # pylint: disable=try-except-raise
raise
except Exception as err: # pylint: disable=broad-except
if 'Connect call failed' in str(err):
log.warning("Could not connect to %s:%d", *session.server)
else:
log.exception("Connecting to %s:%d raised an exception:", *session.server)
self._dead_servers.append(session)
self.sessions.remove(session)
async def pick_fastest_server(self):
self._lost_master.set()
while not self.online:
await asyncio.sleep(0.1)
async def _probe(session):
await session.send_request('server.banner')
return session
done, pending = await asyncio.wait([
_probe(session)
for session in self.sessions if not session.is_closing()
], return_when='FIRST_COMPLETED')
for task in pending:
task.cancel()
for session in done:
return await session