SSE server reliability improvements

This commit is contained in:
Ian Renton
2026-06-05 16:06:33 +01:00
parent af1974f36d
commit a2dff07c0e
12 changed files with 93 additions and 42 deletions

View File

@@ -246,8 +246,9 @@ To set up nginx as a reverse proxy that sits in front of Spothole, first ensure
Create a file at `/etc/nginx/sites-available/` called `spothole`. Give it the following contents, replacing `spothole.app` with the domain name on which you want to run Spothole. If you changed the port on which Spothole runs, update that on the "proxy_pass" line too. Create a file at `/etc/nginx/sites-available/` called `spothole`. Give it the following contents, replacing `spothole.app` with the domain name on which you want to run Spothole. If you changed the port on which Spothole runs, update that on the "proxy_pass" line too.
```nginx ```nginx
map $request_uri $xssorigin { map $request_uri $cors_origin {
~^/api *; ~^/api *;
default "";
} }
server { server {
@@ -258,12 +259,46 @@ server {
alias /var/www/html/.well-known/; alias /var/www/html/.well-known/;
} }
# SSE endpoints
location ~ ^/api/v1/(spots|alerts)/stream {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass http://127.0.0.1:8081;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 24h;
proxy_connect_timeout 10s;
proxy_send_timeout 24h;
proxy_set_header X-Accel-Buffering no;
proxy_hide_header Access-Control-Allow-Origin;
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Cache-Control no-store always;
add_header Content-Type text/event-stream always;
}
# Other API endpoints
location /api/ {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass http://127.0.0.1:8081;
proxy_buffering on;
proxy_cache off;
proxy_read_timeout 30s;
proxy_connect_timeout 10s;
proxy_hide_header Access-Control-Allow-Origin;
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Cache-Control no-store always;
}
# Static assets
location / { location / {
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Connection ""; proxy_set_header Connection "";
proxy_pass http://127.0.0.1:8080; proxy_pass http://127.0.0.1:8081;
proxy_hide_header Access-Control-Allow-Origin; proxy_buffering on;
add_header Access-Control-Allow-Origin $xssorigin; proxy_read_timeout 30s;
proxy_connect_timeout 10s;
add_header Cache-Control "public, max-age=3600, must-revalidate" always;
} }
} }
``` ```

View File

@@ -98,6 +98,9 @@ class APIAlertsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
self._heartbeat = tornado.ioloop.PeriodicCallback(self._callback, SSE_HANDLER_QUEUE_CHECK_INTERVAL) self._heartbeat = tornado.ioloop.PeriodicCallback(self._callback, SSE_HANDLER_QUEUE_CHECK_INTERVAL)
self._heartbeat.start() self._heartbeat.start()
# Flush headers immediately so nginx doesn't time out waiting for a response
self.write_message(msg="", event="keepalive")
except Exception as e: except Exception as e:
logging.warning("Exception when serving SSE socket", e) logging.warning("Exception when serving SSE socket", e)
@@ -122,14 +125,19 @@ class APIAlertsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
try: try:
if self._alert_queue: if self._alert_queue:
while not self._alert_queue.empty(): if not self._alert_queue.empty():
alert = self._alert_queue.get() while not self._alert_queue.empty():
# If the new alert matches our param filters, send it to the client. If not, ignore it. alert = self._alert_queue.get()
if alert_allowed_by_query(alert, self._query_params): # If the new alert matches our param filters, send it to the client. If not, ignore it.
if self._credentials: if alert_allowed_by_query(alert, self._query_params):
alert = copy.deepcopy(alert) if self._credentials:
alert.infer_missing(self._credentials) alert = copy.deepcopy(alert)
self.write_message(msg=json.dumps(alert, default=serialize_everything)) alert.infer_missing(self._credentials)
self.write_message(msg=json.dumps(alert, default=serialize_everything))
else:
# Send a keepalive comment if the queue was empty
self.write_message(msg="", event="keepalive")
if self._alert_queue not in self._sse_alert_queues: if self._alert_queue not in self._sse_alert_queues:
logging.error("Web server cleared up a queue of an active connection!") logging.error("Web server cleared up a queue of an active connection!")

View File

@@ -100,6 +100,9 @@ class APISpotsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
self._heartbeat = tornado.ioloop.PeriodicCallback(self._callback, SSE_HANDLER_QUEUE_CHECK_INTERVAL) self._heartbeat = tornado.ioloop.PeriodicCallback(self._callback, SSE_HANDLER_QUEUE_CHECK_INTERVAL)
self._heartbeat.start() self._heartbeat.start()
# Flush headers immediately so nginx doesn't time out waiting for a response
self.write_message(msg="", event="keepalive")
except Exception as e: except Exception as e:
logging.warning("Exception when serving SSE socket", e) logging.warning("Exception when serving SSE socket", e)
@@ -124,14 +127,19 @@ class APISpotsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
try: try:
if self._spot_queue: if self._spot_queue:
while not self._spot_queue.empty(): if not self._spot_queue.empty():
spot = self._spot_queue.get() while not self._spot_queue.empty():
# If the new spot matches our param filters, send it to the client. If not, ignore it. spot = self._spot_queue.get()
if spot_allowed_by_query(spot, self._query_params): # If the new spot matches our param filters, send it to the client. If not, ignore it.
if self._credentials: if spot_allowed_by_query(spot, self._query_params):
spot = copy.deepcopy(spot) if self._credentials:
spot.infer_missing(self._credentials) spot = copy.deepcopy(spot)
self.write_message(msg=json.dumps(spot, default=serialize_everything)) spot.infer_missing(self._credentials)
self.write_message(msg=json.dumps(spot, default=serialize_everything))
else:
# Send a keepalive comment if the queue was empty
self.write_message(msg="", event="keepalive")
if self._spot_queue not in self._sse_spot_queues: if self._spot_queue not in self._sse_spot_queues:
logging.error("Web server cleared up a queue of an active connection!") logging.error("Web server cleared up a queue of an active connection!")

View File

@@ -69,7 +69,7 @@
<p>This software is dedicated to the memory of Tom G1PJB, SK, a friend and colleague who sadly passed away around the time I started writing it in Autumn 2025. I was looking forward to showing it to you when it was done.</p> <p>This software is dedicated to the memory of Tom G1PJB, SK, a friend and colleague who sadly passed away around the time I started writing it in Autumn 2025. I was looking forward to showing it to you when it was done.</p>
</div> </div>
<script src="/js/common.js?v=1780424170"></script> <script src="/js/common.js?v=1780671994"></script>
<script>$(document).ready(function() { $("#nav-link-about").addClass("active"); }); <!-- highlight active page in nav --></script> <script>$(document).ready(function() { $("#nav-link-about").addClass("active"); }); <!-- highlight active page in nav --></script>
{% end %} {% end %}

View File

@@ -69,8 +69,8 @@
</div> </div>
<script src="/js/common.js?v=1780424170"></script> <script src="/js/common.js?v=1780671994"></script>
<script src="/js/add-spot.js?v=1780424170"></script> <script src="/js/add-spot.js?v=1780671994"></script>
<script>$(document).ready(function() { $("#nav-link-add-spot").addClass("active"); }); <!-- highlight active page in nav --></script> <script>$(document).ready(function() { $("#nav-link-add-spot").addClass("active"); }); <!-- highlight active page in nav --></script>
{% end %} {% end %}

View File

@@ -70,8 +70,8 @@
</div> </div>
<script src="/js/common.js?v=1780424170"></script> <script src="/js/common.js?v=1780671994"></script>
<script src="/js/alerts.js?v=1780424170"></script> <script src="/js/alerts.js?v=1780671994"></script>
<script>$(document).ready(function() { $("#nav-link-alerts").addClass("active"); }); <!-- highlight active page in nav --></script> <script>$(document).ready(function() { $("#nav-link-alerts").addClass("active"); }); <!-- highlight active page in nav --></script>
{% end %} {% end %}

View File

@@ -76,9 +76,9 @@
<script> <script>
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %}; let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
</script> </script>
<script src="/js/common.js?v=1780424170"></script> <script src="/js/common.js?v=1780671994"></script>
<script src="/js/spotsbandsandmap.js?v=1780424170"></script> <script src="/js/spotsbandsandmap.js?v=1780671994"></script>
<script src="/js/bands.js?v=1780424170"></script> <script src="/js/bands.js?v=1780671994"></script>
<script>$(document).ready(function() { $("#nav-link-bands").addClass("active"); }); <!-- highlight active page in nav --></script> <script>$(document).ready(function() { $("#nav-link-bands").addClass("active"); }); <!-- highlight active page in nav --></script>
{% end %} {% end %}

View File

@@ -1,6 +1,6 @@
{% extends "skeleton.html" %} {% extends "skeleton.html" %}
{% block head_extra %} {% block head_extra %}
<link rel="stylesheet" href="/css/style.css?v=1780424170" type="text/css"> <link rel="stylesheet" href="/css/style.css?v=1780671993" type="text/css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous"> integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
<link href="/fa/css/fontawesome.min.css" rel="stylesheet" /> <link href="/fa/css/fontawesome.min.css" rel="stylesheet" />
@@ -19,9 +19,9 @@
integrity="sha384-L1eE4eD41kpBIWe2I0eHy+GnEUC4RIpcvibVW2JCminuPlTl+2Bc528iPdVMg5Dn" integrity="sha384-L1eE4eD41kpBIWe2I0eHy+GnEUC4RIpcvibVW2JCminuPlTl+2Bc528iPdVMg5Dn"
crossorigin="anonymous"></script> crossorigin="anonymous"></script>
<script src="https://misc.ianrenton.com/jsutils/utils.js?v=1780424170"></script> <script src="https://misc.ianrenton.com/jsutils/utils.js?v=1780671993"></script>
<script src="https://misc.ianrenton.com/jsutils/ui-ham.js?v=1780424170"></script> <script src="https://misc.ianrenton.com/jsutils/ui-ham.js?v=1780671993"></script>
<script src="https://misc.ianrenton.com/jsutils/geo.js?v=1780424170"></script> <script src="https://misc.ianrenton.com/jsutils/geo.js?v=1780671993"></script>
{% end %} {% end %}
{% block body %} {% block body %}
<div class="container"> <div class="container">

View File

@@ -271,8 +271,8 @@
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.9/dist/chart.umd.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.9/dist/chart.umd.min.js"></script>
<script src="/js/common.js?v=1780424170"></script> <script src="/js/common.js?v=1780671994"></script>
<script src="/js/conditions.js?v=1780424170"></script> <script src="/js/conditions.js?v=1780671994"></script>
<script>$(document).ready(function () { <script>$(document).ready(function () {
$("#nav-link-conditions").addClass("active"); $("#nav-link-conditions").addClass("active");
}); <!-- highlight active page in nav --></script> }); <!-- highlight active page in nav --></script>

View File

@@ -94,9 +94,9 @@
<script> <script>
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %}; let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
</script> </script>
<script src="/js/common.js?v=1780424170"></script> <script src="/js/common.js?v=1780671994"></script>
<script src="/js/spotsbandsandmap.js?v=1780424170"></script> <script src="/js/spotsbandsandmap.js?v=1780671994"></script>
<script src="/js/map.js?v=1780424170"></script> <script src="/js/map.js?v=1780671994"></script>
<script>$(document).ready(function() { $("#nav-link-map").addClass("active"); }); <!-- highlight active page in nav --></script> <script>$(document).ready(function() { $("#nav-link-map").addClass("active"); }); <!-- highlight active page in nav --></script>
{% end %} {% end %}

View File

@@ -104,9 +104,9 @@
<script> <script>
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %}; let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
</script> </script>
<script src="/js/common.js?v=1780424170"></script> <script src="/js/common.js?v=1780671993"></script>
<script src="/js/spotsbandsandmap.js?v=1780424170"></script> <script src="/js/spotsbandsandmap.js?v=1780671993"></script>
<script src="/js/spots.js?v=1780424170"></script> <script src="/js/spots.js?v=1780671993"></script>
<script>$(document).ready(function() { $("#nav-link-spots").addClass("active"); }); <!-- highlight active page in nav --></script> <script>$(document).ready(function() { $("#nav-link-spots").addClass("active"); }); <!-- highlight active page in nav --></script>
{% end %} {% end %}

View File

@@ -59,8 +59,8 @@
</div> </div>
</div> </div>
<script src="/js/common.js?v=1780424170"></script> <script src="/js/common.js?v=1780671994"></script>
<script src="/js/status.js?v=1780424170"></script> <script src="/js/status.js?v=1780671994"></script>
<script> <script>
$(document).ready(function() { $("#nav-link-status").addClass("active"); }); <!-- highlight active page in nav --> $(document).ready(function() { $("#nav-link-status").addClass("active"); }); <!-- highlight active page in nav -->
</script> </script>