diff --git a/flexmeasures/app.py b/flexmeasures/app.py index 059eee3eba..891fd51c08 100644 --- a/flexmeasures/app.py +++ b/flexmeasures/app.py @@ -52,6 +52,10 @@ def create( # noqa C901 load_dotenv() app = Flask("flexmeasures") + from flexmeasures.ws import sock + + sock.init_app(app) + if env is not None: # overwrite app.config["FLEXMEASURES_ENV"] = env if app.config.get("FLEXMEASURES_ENV") == "testing": diff --git a/flexmeasures/ws/__init__.py b/flexmeasures/ws/__init__.py new file mode 100644 index 0000000000..f565dea771 --- /dev/null +++ b/flexmeasures/ws/__init__.py @@ -0,0 +1,19 @@ +import importlib +import pkgutil +from flask import Blueprint, current_app +from simple_websocket import Server +from flask_security import auth_token_required + +from flask_sock import Sock + +sock = Sock() + + +def import_all_modules(package_name): + package = importlib.import_module(package_name) + for _, name, _ in pkgutil.iter_modules(package.__path__): + importlib.import_module(f"{package_name}.{name}") + + +# we need to import all the modules to run the route decorators +import_all_modules("flexmeasures.ws") diff --git a/flexmeasures/ws/ping1.py b/flexmeasures/ws/ping1.py new file mode 100644 index 0000000000..02afaacd5c --- /dev/null +++ b/flexmeasures/ws/ping1.py @@ -0,0 +1,23 @@ +import logging +from flexmeasures.ws import sock +from flask import current_app +from flexmeasures import Sensor +from sqlalchemy import select + +logger = logging.getLogger(__name__) + + +@sock.route("/ping1") +def echo1(ws): + while True: + with current_app.app_context(): + data = ws.receive() + + if data == "close": + break + + sensors = current_app.db.session.execute( + select(Sensor).where(Sensor.id == 1) + ).scalar() + + ws.send(str(sensors.__dict__)) diff --git a/flexmeasures/ws/ping2.py b/flexmeasures/ws/ping2.py new file mode 100644 index 0000000000..a072f3f600 --- /dev/null +++ b/flexmeasures/ws/ping2.py @@ -0,0 +1,14 @@ +import logging +from flexmeasures.ws import sock + +logger = logging.Logger(__name__) + + +@sock.route("/ping2") +def echo2(ws): + while True: + data = ws.receive() + logger.error("ping2>" + data) + if data == "close": + break + ws.send(data) diff --git a/flexmeasures/ws/v1.py b/flexmeasures/ws/v1.py new file mode 100644 index 0000000000..eab8a10fee --- /dev/null +++ b/flexmeasures/ws/v1.py @@ -0,0 +1,37 @@ +import logging +from flexmeasures.ws import sock +from flask import current_app +from flexmeasures import Sensor +from sqlalchemy import select, func +import json + +logger = logging.Logger(__name__) + + +@sock.route("/v1") +def header_test(ws): + # Get all headers + all_headers = { + k[5:].lower().replace("_", "-"): v + for k, v in ws.environ.items() + if k.startswith("HTTP_") + } + + # Get specific header if needed + custom_header = ws.environ.get("HTTP_X_CUSTOM_HEADER") + + logger.info(f"All headers: {all_headers}") + logger.info(f"Custom header: {custom_header}") + + # Send initial message with metadata + ws.send( + json.dumps({"type": "metadata", "headers": {"X-Server-Header": "ServerValue"}}) + ) + + while True: + data = ws.receive() + logger.error("v1>" + data) + if data == "close": + break + sensors = current_app.db.session.execute(select(func.count(Sensor.id))).scalar() + ws.send(str(sensors)) diff --git a/notebooks/websocket_analysis.ipynb b/notebooks/websocket_analysis.ipynb new file mode 100644 index 0000000000..03fbbcf178 --- /dev/null +++ b/notebooks/websocket_analysis.ipynb @@ -0,0 +1,206 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 64, + "id": "2b691e65-0818-438a-b484-9ce439baef44", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import pandas as pd\n", + "\n", + "import plotly.offline as pyo\n", + "import plotly.graph_objs as go\n", + "import plotly.io as pio\n", + "\n", + "\n", + "import plotly_express as px\n", + "pio.renderers.default = 'iframe'\n", + "\n", + "data = pd.read_csv(\"results-db-get-sensor-1.csv\", names=[\"time\", \"type\", \"id\", \"delta\"])\n", + "data[\"time\"] = data[\"time\"].apply(lambda x: pd.Timestamp.fromtimestamp(x))\n", + "data = data.dropna()\n", + "data[\"id2\"] = data.apply(lambda x: f\"{x['type']}: {x['id']}\", axis=1)\n", + "fig = px.line(data, x=\"time\", y=\"delta\", color=\"type\", labels={\n", + " \"time\" : \"Time\",\n", + " \"delta\": \"Roundtrip Time (s)\",\n", + " \"type\" : \"Protocol\"\n", + "}, title=\"Roundtrip Time with 1000 concurrent WS connections @ 1Hz and 1000 concurrent API requests @ 1Hz\")\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "id": "3a8f9091-b8bd-4802-af36-bb01a2942f9a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
| \n", + " | id | \n", + "
|---|---|
| type | \n", + "\n", + " |
| API | \n", + "5 | \n", + "
| WS | \n", + "623 | \n", + "