bigchaindb/bigchaindb/web/websocket_server.py

187 lines
5.1 KiB
Python
Raw Normal View History

# Copyright © 2020 Interplanetary Database Association e.V.,
# BigchainDB and IPDB software contributors.
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
# Code is Apache-2.0 and docs are CC-BY-4.0
2017-03-30 17:27:03 +02:00
"""WebSocket server for the BigchainDB Event Stream API."""
2017-04-07 09:16:22 +02:00
# NOTE
#
# This module contains some functions and utilities that might belong to other
# modules. For now, I prefer to keep everything in this module. Why? Because
# those functions are needed only here.
#
# When we will extend this part of the project and we find that we need those
# functionalities elsewhere, we can start creating new modules and organizing
# things in a better way.
import json
2017-03-30 17:27:03 +02:00
import asyncio
2017-03-31 15:56:29 +02:00
import logging
2017-04-07 09:16:22 +02:00
import threading
2017-03-30 17:27:03 +02:00
from uuid import uuid4
from concurrent.futures import CancelledError
2017-03-30 17:27:03 +02:00
2017-03-31 15:56:29 +02:00
import aiohttp
2017-03-30 17:27:03 +02:00
from aiohttp import web
2017-04-07 10:51:00 +02:00
from bigchaindb import config
2017-04-07 09:16:22 +02:00
from bigchaindb.events import EventTypes
2017-03-30 17:27:03 +02:00
2017-03-31 15:56:29 +02:00
logger = logging.getLogger(__name__)
POISON_PILL = 'POISON_PILL'
2017-06-14 10:36:23 +02:00
EVENTS_ENDPOINT = '/api/v1/streams/valid_transactions'
2017-04-07 09:16:22 +02:00
def _multiprocessing_to_asyncio(in_queue, out_queue, loop):
"""Bridge between a synchronous multiprocessing queue
and an asynchronous asyncio queue.
Args:
in_queue (multiprocessing.Queue): input queue
out_queue (asyncio.Queue): output queue
"""
while True:
value = in_queue.get()
loop.call_soon_threadsafe(out_queue.put_nowait, value)
2017-03-30 17:27:03 +02:00
def eventify_block(block):
for tx in block['transactions']:
if tx.asset:
asset_id = tx.asset.get('id', tx.id)
else:
asset_id = tx.id
yield {'height': block['height'],
'asset_id': asset_id,
'transaction_id': tx.id}
2017-03-30 17:27:03 +02:00
class Dispatcher:
2017-03-31 15:56:29 +02:00
"""Dispatch events to websockets.
This class implements a simple publish/subscribe pattern.
"""
2017-03-30 17:27:03 +02:00
def __init__(self, event_source):
2017-03-31 15:56:29 +02:00
"""Create a new instance.
Args:
event_source: a source of events. Elements in the queue
should be strings.
"""
2017-03-30 17:27:03 +02:00
self.event_source = event_source
self.subscribers = {}
2017-04-03 11:48:48 +02:00
def subscribe(self, uuid, websocket):
2017-03-31 15:56:29 +02:00
"""Add a websocket to the list of subscribers.
Args:
uuid (str): a unique identifier for the websocket.
2017-04-03 11:48:48 +02:00
websocket: the websocket to publish information.
2017-03-31 15:56:29 +02:00
"""
2017-04-03 11:48:48 +02:00
self.subscribers[uuid] = websocket
2017-03-30 17:27:03 +02:00
2017-10-19 12:19:43 +02:00
def unsubscribe(self, uuid):
"""Remove a websocket from the list of subscribers.
Args:
uuid (str): a unique identifier for the websocket.
"""
del self.subscribers[uuid]
2017-03-30 17:27:03 +02:00
@asyncio.coroutine
def publish(self):
2017-03-31 15:56:29 +02:00
"""Publish new events to the subscribers."""
2017-03-30 17:27:03 +02:00
while True:
event = yield from self.event_source.get()
2017-04-07 09:16:22 +02:00
str_buffer = []
2017-03-30 17:27:03 +02:00
if event == POISON_PILL:
return
2017-04-07 09:16:22 +02:00
if isinstance(event, str):
str_buffer.append(event)
elif event.type == EventTypes.BLOCK_VALID:
str_buffer = map(json.dumps, eventify_block(event.data))
2017-04-07 09:16:22 +02:00
for str_item in str_buffer:
for _, websocket in self.subscribers.items():
yield from websocket.send_str(str_item)
2017-03-30 17:27:03 +02:00
@asyncio.coroutine
def websocket_handler(request):
2017-03-31 15:56:29 +02:00
"""Handle a new socket connection."""
logger.debug('New websocket connection.')
2017-04-03 11:48:48 +02:00
websocket = web.WebSocketResponse()
yield from websocket.prepare(request)
2017-03-30 17:27:03 +02:00
uuid = uuid4()
2017-04-03 11:48:48 +02:00
request.app['dispatcher'].subscribe(uuid, websocket)
2017-03-31 15:56:29 +02:00
2017-03-30 17:27:03 +02:00
while True:
# Consume input buffer
2017-06-14 11:49:05 +02:00
try:
msg = yield from websocket.receive()
except RuntimeError as e:
logger.debug('Websocket exception: %s', str(e))
2017-10-19 12:19:43 +02:00
break
except CancelledError:
logger.debug('Websocket closed')
break
2017-10-19 12:19:43 +02:00
if msg.type == aiohttp.WSMsgType.CLOSED:
logger.debug('Websocket closed')
break
elif msg.type == aiohttp.WSMsgType.ERROR:
2017-04-03 11:48:48 +02:00
logger.debug('Websocket exception: %s', websocket.exception())
2017-10-19 12:19:43 +02:00
break
request.app['dispatcher'].unsubscribe(uuid)
return websocket
2017-03-30 17:27:03 +02:00
2017-04-03 11:48:48 +02:00
def init_app(event_source, *, loop=None):
2017-03-31 15:56:29 +02:00
"""Init the application server.
Return:
An aiohttp application.
"""
2017-03-30 17:27:03 +02:00
dispatcher = Dispatcher(event_source)
# Schedule the dispatcher
loop.create_task(dispatcher.publish())
app = web.Application(loop=loop)
app['dispatcher'] = dispatcher
2017-04-07 09:16:22 +02:00
app.router.add_get(EVENTS_ENDPOINT, websocket_handler)
2017-03-30 17:27:03 +02:00
return app
2017-03-31 15:56:29 +02:00
2017-04-07 09:16:22 +02:00
def start(sync_event_source, loop=None):
2017-04-03 11:48:48 +02:00
"""Create and start the WebSocket server."""
2017-03-31 15:56:29 +02:00
2017-04-03 11:48:48 +02:00
if not loop:
loop = asyncio.get_event_loop()
event_source = asyncio.Queue(loop=loop)
2017-04-03 11:48:48 +02:00
2017-04-07 09:16:22 +02:00
bridge = threading.Thread(target=_multiprocessing_to_asyncio,
args=(sync_event_source, event_source, loop),
daemon=True)
bridge.start()
2017-04-03 11:48:48 +02:00
2017-04-07 09:16:22 +02:00
app = init_app(event_source, loop=loop)
2017-04-07 10:51:00 +02:00
aiohttp.web.run_app(app,
host=config['wsserver']['host'],
port=config['wsserver']['port'])