diff --git a/.idea/modules.xml b/.idea/modules.xml
index c335631..fe999aa 100644
--- a/.idea/modules.xml
+++ b/.idea/modules.xml
@@ -2,7 +2,7 @@
-
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/Run.xml b/.idea/runConfigurations/Run.xml
index 56151cd..cf1858b 100644
--- a/.idea/runConfigurations/Run.xml
+++ b/.idea/runConfigurations/Run.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/.idea/Spothole.iml b/.idea/spothole.iml
similarity index 100%
rename from .idea/Spothole.iml
rename to .idea/spothole.iml
diff --git a/core/constants.py b/core/constants.py
index 4057e5a..442a961 100644
--- a/core/constants.py
+++ b/core/constants.py
@@ -3,7 +3,7 @@ from data.band import Band
from data.sig import SIG
# General software
-SOFTWARE_VERSION = "2.0.4"
+SOFTWARE_VERSION = "2.0.5"
# HTTP headers used for spot providers that use HTTP
HTTP_HEADERS = {"User-Agent": f"Spothole v{SOFTWARE_VERSION} (operated by {SERVER_OWNER_CALLSIGN})"}
diff --git a/core/data_store.py b/core/data_store.py
index aea5359..ea8aa3e 100644
--- a/core/data_store.py
+++ b/core/data_store.py
@@ -6,6 +6,7 @@ import diskcache
from core.config import MAX_ALERT_AGE, MAX_SPOT_AGE
from core.live_data_cache import LiveDataCache
+from core.single_object_data_cache import SingleObjectDataCache
from data.solar_conditions import SolarConditions
logger = logging.getLogger(__name__)
@@ -34,10 +35,8 @@ class DataStore:
self.dxcc_data = None
self.dxcc_lookup_by_call_regex = []
self.sigrefs = None
- self.status_data = {}
- self._status = None
- self.solar_conditions = {}
- self._solar = None
+ self.status = None
+ self.solar_conditions = None
# ITU/CQ zone GeoJSON data is only ever loaded statically from a local file so these don't even need to be
# caches, they can just be straight objects
self.cq_zone_data = None
@@ -46,16 +45,11 @@ class DataStore:
def setup(self):
Path(CACHE_DIR).mkdir(parents=True, exist_ok=True)
- # Standard disk cache for solar data and status data, but each cache contains only a single object which we
- # expose to the wider application
- self._solar = diskcache.Cache(f"{CACHE_DIR}solar")
- if "solar_conditions" not in self._solar:
- self._solar.add("solar_conditions", SolarConditions())
- self.solar_conditions = self._solar.get("solar_conditions")
- self._status = diskcache.Cache(f"{CACHE_DIR}status")
- if "status_data" not in self._status:
- self._status.add("status_data", {})
- self.status_data = self._status.get("status_data")
+ # For solar data and status data, we use a wrapper around disk cache where each cache contains only a single
+ # object exposed to the wider application, and provides a store() method for callers to notify diskcache that
+ # the object has changed and needs to be re-cached.
+ self.solar_conditions = SingleObjectDataCache(f"{CACHE_DIR}solar", SolarConditions())
+ self.status = SingleObjectDataCache(f"{CACHE_DIR}status", {})
# Standard disk cache for static reference and SIG ref data. Separate provider threads will repopulate these on
# a regular basis but there's no need for a TTL since old data is better than no data.
@@ -116,13 +110,10 @@ class DataStore:
self.dxcc_lookup_by_call_regex.append((re.compile(entry["prefixRegex"]), entry["entityCode"]))
def close(self):
- self.spots.save_snapshot()
- self.alerts.save_snapshot()
-
self.spots.close()
self.alerts.close()
- self._solar.close()
- self._status.close()
+ self.solar.close()
+ self.status.close()
self.dxcc_data.close()
self.sigrefs.close()
self.callsign_data_countryfiles.close()
diff --git a/core/single_object_data_cache.py b/core/single_object_data_cache.py
new file mode 100644
index 0000000..66b1d7f
--- /dev/null
+++ b/core/single_object_data_cache.py
@@ -0,0 +1,39 @@
+import logging
+import threading
+
+import diskcache
+
+logger = logging.getLogger(__name__)
+
+
+class SingleObjectDataCache:
+ """Cache for status and solar conditions. This uses DiskCache, but unlike the standard DiskCache users like SIG and
+ callsign lookup handlers, status and solar conditions are persisted as a single object. If we just load the object
+ from DiskCache and modify it, DiskCache doesn't know that it's been updated and needs re-caching, so we provide a
+ store() method that any functions updating the object can call afterwards."""
+
+ def __init__(self, cache_dir, object_if_empty):
+ """Initialize a SingleObjectDataCache. Provide the directory to load the cache from and save it to. If the cache
+ is empty, the provided object_if_empty parameter will be used to initialise it."""
+
+ self._lock = threading.Lock()
+ self._cache = diskcache.Cache(cache_dir)
+ # This cache stores a single object, doesn't matter what it's called so "object" will do
+ if "object" not in self._cache:
+ self._cache.add("object", object_if_empty)
+ self._obj = self._cache.get("object")
+
+ def get(self):
+ """Get the data object. This can then be manipulated as necessary across multiple threads. Any function
+ modifying the object must remember to call store() afterwards."""
+ return self._obj
+
+ def store(self):
+ """Store the updated object in the cache. Any function modifying the object must remember to call this
+ afterwards."""
+ with self._lock:
+ self._cache.set("object", self._obj)
+
+ def close(self):
+ self.store()
+ self._cache.close()
diff --git a/core/status_reporter.py b/core/status_reporter.py
index aba0a16..de9e07b 100644
--- a/core/status_reporter.py
+++ b/core/status_reporter.py
@@ -25,8 +25,9 @@ class StatusReporter:
self._stop_event = Event()
self._startup_time = datetime.now(pytz.UTC)
- DATA_STORE.status_data["software_version"] = SOFTWARE_VERSION
- DATA_STORE.status_data["server_owner_callsign"] = SERVER_OWNER_CALLSIGN
+ DATA_STORE.status.get()["software_version"] = SOFTWARE_VERSION
+ DATA_STORE.status.get()["server_owner_callsign"] = SERVER_OWNER_CALLSIGN
+ DATA_STORE.status.store()
def start(self):
"""Start the reporter thread"""
@@ -50,11 +51,11 @@ class StatusReporter:
def _report(self):
"""Write status information"""
- DATA_STORE.status_data["uptime"] = (datetime.now(pytz.UTC) - self._startup_time).total_seconds()
- DATA_STORE.status_data["mem_use_mb"] = round(psutil.Process(os.getpid()).memory_info().rss / (1024 * 1024), 3)
- DATA_STORE.status_data["num_spots"] = len(DATA_STORE.spots.values())
- DATA_STORE.status_data["num_alerts"] = len(DATA_STORE.alerts.values())
- DATA_STORE.status_data["spot_providers"] = [
+ DATA_STORE.status.get()["uptime"] = (datetime.now(pytz.UTC) - self._startup_time).total_seconds()
+ DATA_STORE.status.get()["mem_use_mb"] = round(psutil.Process(os.getpid()).memory_info().rss / (1024 * 1024), 3)
+ DATA_STORE.status.get()["num_spots"] = len(DATA_STORE.spots.values())
+ DATA_STORE.status.get()["num_alerts"] = len(DATA_STORE.alerts.values())
+ DATA_STORE.status.get()["spot_providers"] = [
{
"name": p.name,
"enabled": p.enabled,
@@ -69,7 +70,7 @@ class StatusReporter:
}
for p in DATA_PROVIDERS.spot_providers
]
- DATA_STORE.status_data["alert_providers"] = [
+ DATA_STORE.status.get()["alert_providers"] = [
{
"name": p.name,
"enabled": p.enabled,
@@ -80,7 +81,7 @@ class StatusReporter:
}
for p in DATA_PROVIDERS.alert_providers
]
- DATA_STORE.status_data["solar_condition_providers"] = [
+ DATA_STORE.status.get()["solar_condition_providers"] = [
{
"name": p.name,
"enabled": p.enabled,
@@ -91,7 +92,7 @@ class StatusReporter:
}
for p in DATA_PROVIDERS.solar_condition_providers
]
- DATA_STORE.status_data["static_data_providers"] = [
+ DATA_STORE.status.get()["static_data_providers"] = [
{
"name": p.name,
"enabled": p.enabled,
@@ -102,7 +103,7 @@ class StatusReporter:
}
for p in DATA_PROVIDERS.static_data_providers
]
- DATA_STORE.status_data["sig_ref_data_providers"] = [
+ DATA_STORE.status.get()["sig_ref_data_providers"] = [
{
"sig_name": p.sig_name,
"enabled": p.enabled,
@@ -114,7 +115,7 @@ class StatusReporter:
}
for p in DATA_PROVIDERS.sig_ref_data_providers
]
- DATA_STORE.status_data["callsign_data_providers"] = [
+ DATA_STORE.status.get()["callsign_data_providers"] = [
{
"name": p.name,
"enabled": p.enabled,
@@ -126,13 +127,13 @@ class StatusReporter:
}
for p in DATA_PROVIDERS.callsign_data_providers
]
- DATA_STORE.status_data["cleanup"] = {
+ DATA_STORE.status.get()["cleanup"] = {
"status": CLEANUP_TIMER.status,
"last_ran": CLEANUP_TIMER.last_cleanup_time.replace(tzinfo=pytz.UTC).timestamp()
if CLEANUP_TIMER.last_cleanup_time
else 0,
}
- DATA_STORE.status_data["webserver"] = {
+ DATA_STORE.status.get()["webserver"] = {
"status": WEB_SERVER.web_server_metrics["status"],
"last_api_access": WEB_SERVER.web_server_metrics["last_api_access_time"]
.replace(tzinfo=pytz.UTC)
@@ -147,6 +148,7 @@ class StatusReporter:
else 0,
"page_access_count": WEB_SERVER.web_server_metrics["page_access_counter"],
}
+ DATA_STORE.status.store()
# Update Prometheus metrics
memory_use_gauge.set(psutil.Process(os.getpid()).memory_info().rss)
diff --git a/providers/solarconditions/solar_conditions_provider.py b/providers/solarconditions/solar_conditions_provider.py
index 79d454e..94add91 100644
--- a/providers/solarconditions/solar_conditions_provider.py
+++ b/providers/solarconditions/solar_conditions_provider.py
@@ -16,7 +16,7 @@ class SolarConditionsProvider:
self.enabled = provider_config.get("enabled", True)
self.last_update_time = datetime.min.replace(tzinfo=pytz.UTC)
self.status = "Not Started" if self.enabled else "Disabled"
- self._solar_conditions = DATA_STORE.solar_conditions
+ self._solar_conditions = DATA_STORE.solar_conditions.get()
def start(self):
"""Start the provider. This should return immediately after spawning threads to access the remote resources"""
@@ -36,3 +36,4 @@ class SolarConditionsProvider:
if hasattr(self._solar_conditions, key):
setattr(self._solar_conditions, key, value)
self._solar_conditions.infer_descriptions()
+ DATA_STORE.solar_conditions.store()
diff --git a/pyproject.toml b/pyproject.toml
index 1ce1367..795370e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "spothole"
-version = "2.0.4"
+version = "2.0.5"
authors = [
{ name = "Ian Renton", email = "ian@ianrenton.com" },
]
diff --git a/server/webserver.py b/server/webserver.py
index 9db01f8..e9222fe 100644
--- a/server/webserver.py
+++ b/server/webserver.py
@@ -111,7 +111,7 @@ class WebServer:
(
r"/api/v2/solar",
APISolarConditionsHandler,
- {"solar_conditions": self._data_store.solar_conditions, **handler_opts},
+ {"solar_conditions": self._data_store.solar_conditions.get(), **handler_opts},
),
(
r"/api/v2/dxstats",
@@ -121,12 +121,12 @@ class WebServer:
(
r"/api/v2/options",
APIOptionsHandler,
- {"status_data": self._data_store.status_data, **handler_opts},
+ {"status_data": self._data_store.status.get(), **handler_opts},
),
(
r"/api/v2/status",
APIStatusHandler,
- {"status_data": self._data_store.status_data, **handler_opts},
+ {"status_data": self._data_store.status.get(), **handler_opts},
),
(r"/api/v2/lookup/call", APILookupCallHandler, {**handler_opts}),
(r"/api/v2/lookup/sigref", APILookupSIGRefHandler, {**handler_opts}),
diff --git a/templates/add_spot.html b/templates/add_spot.html
index 587a0cd..6e70c06 100644
--- a/templates/add_spot.html
+++ b/templates/add_spot.html
@@ -76,7 +76,7 @@
-
+
diff --git a/templates/alerts.html b/templates/alerts.html
index e76ad98..ae4563e 100644
--- a/templates/alerts.html
+++ b/templates/alerts.html
@@ -84,7 +84,7 @@
-
+
diff --git a/templates/bands.html b/templates/bands.html
index 2898110..0189ffe 100644
--- a/templates/bands.html
+++ b/templates/bands.html
@@ -76,8 +76,8 @@
-
-
+
+
diff --git a/templates/base.html b/templates/base.html
index a90ed07..e4d148a 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -1,6 +1,6 @@
{% extends "skeleton.html" %}
{% block head_extra %}
-
+
@@ -15,10 +15,10 @@
window.fetchEventSource = fetchEventSource;
-
-
-
-
+
+
+
+
{% end %}
{% block body %}