mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-08-06 02:21:42 +00:00
Improve use of URL cache #118
This commit is contained in:
+9
-9
@@ -4,17 +4,17 @@ from pathlib import Path
|
||||
import diskcache
|
||||
|
||||
from core.config import MAX_SPOT_AGE, MAX_ALERT_AGE
|
||||
from core.constants import SIGS
|
||||
from core.live_data_cache import LiveDataCache
|
||||
from data.solar_conditions import SolarConditions
|
||||
|
||||
CACHE_DIR = "./cache/"
|
||||
|
||||
|
||||
class DataStore:
|
||||
"""Data caching/storage object. Handles storage of spots, alerts, solar conditions, SIG reference data, and callsign
|
||||
lookup data using different caching strategies for each."""
|
||||
|
||||
def __init__(self):
|
||||
self._CACHE_DIR = "./cache"
|
||||
self._MAX_SPOT_COUNT = 100000
|
||||
self._MAX_ALERT_COUNT = 100000
|
||||
self._SPOT_ALERT_SNAPSHOT_INTERVAL_SEC = 300
|
||||
@@ -29,15 +29,15 @@ class DataStore:
|
||||
self._solar = None
|
||||
|
||||
def setup(self):
|
||||
Path(self._CACHE_DIR).mkdir(parents=True, exist_ok=True)
|
||||
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(self._CACHE_DIR + "/solar")
|
||||
self._solar = diskcache.Cache(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(self._CACHE_DIR + "/status")
|
||||
self._status = diskcache.Cache(CACHE_DIR + "status")
|
||||
if "status_data" not in self._status:
|
||||
self._status.add("status_data", {})
|
||||
self.status_data = self._status.get("status_data")
|
||||
@@ -46,25 +46,25 @@ class DataStore:
|
||||
# but there's no need for a TTL since old data is better than no data. We need to key on both SIG and reference,
|
||||
# and trying to do two layers of dict in diskcache absolutely destroys performance with unpickling huge dicts,
|
||||
# so we have an ugly "SIG:ref" syntax for keys to keep it a single level.
|
||||
self.sigrefs = diskcache.Cache(self._CACHE_DIR + "/sigrefs")
|
||||
self.sigrefs = diskcache.Cache(CACHE_DIR + "sigrefs")
|
||||
logging.info(f"Loaded data for %d SIG references.", len(self.sigrefs))
|
||||
|
||||
# Standard disk cache for callsign data. This data does have a TTL to trigger an occasional re-lookup.
|
||||
# Old data *is* better than no data, but we can't have a background thread re-looking-up every callsign
|
||||
# we've seen, so we rely on them timing out and this triggering another lookup.
|
||||
self.callsigns = diskcache.Cache(self._CACHE_DIR + "/callsigns")
|
||||
self.callsigns = diskcache.Cache(CACHE_DIR + "callsigns")
|
||||
logging.info(f"Loaded data for %d callsigns.", len(self.callsigns))
|
||||
|
||||
# Special caches for spots and alerts, which have TTL and write snapshots to disk at an interval. We
|
||||
# specifically load these caches *last* so that any sigref and callsign data is already loaded from disk cache
|
||||
# before the spots and alerts are live in the system.
|
||||
self.spots = LiveDataCache(maxsize=self._MAX_SPOT_COUNT, ttl=MAX_SPOT_AGE,
|
||||
snapshot_dir=self._CACHE_DIR + "/spots",
|
||||
snapshot_dir=CACHE_DIR + "spots",
|
||||
snapshot_interval_sec=self._SPOT_ALERT_SNAPSHOT_INTERVAL_SEC)
|
||||
logging.info(f"Loaded %d spots from a previous run.", len(self.spots.keys()))
|
||||
|
||||
self.alerts = LiveDataCache(maxsize=self._MAX_ALERT_COUNT, ttl=MAX_ALERT_AGE,
|
||||
snapshot_dir=self._CACHE_DIR + "/alerts",
|
||||
snapshot_dir=CACHE_DIR + "alerts",
|
||||
snapshot_interval_sec=self._SPOT_ALERT_SNAPSHOT_INTERVAL_SEC)
|
||||
logging.info(f"Loaded %d alerts from a previous run.", len(self.alerts.keys()))
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ from requests_cache import CachedSession
|
||||
from core.config import config
|
||||
from core.constants import BANDS, UNKNOWN_BAND, CW_MODES, PHONE_MODES, DATA_MODES, ALL_MODES, \
|
||||
HTTP_HEADERS, HAMQTH_PRG, MODE_ALIASES
|
||||
from core.url_data_cache import URL_DATA_CACHE
|
||||
from core.url_data_cache import URLDataCache
|
||||
|
||||
# QRZ XML field names differ from pyhamtools' normalised names; map them here.
|
||||
_QRZ_FIELD_MAP = {
|
||||
@@ -29,6 +29,7 @@ _QRZ_FIELD_MAP = {
|
||||
}
|
||||
_QRZ_INT_FIELDS = {"adif", "cqz", "ituz"}
|
||||
_QRZ_FLOAT_FIELDS = {"latitude", "longitude"}
|
||||
_URL_DATA_CACHE = URLDataCache("callsign_lookup")
|
||||
|
||||
|
||||
def _normalize_qrz_data(raw):
|
||||
@@ -142,7 +143,7 @@ class LookupHelper:
|
||||
|
||||
try:
|
||||
logging.info("Downloading Country-files.com cty.plist...")
|
||||
response = URL_DATA_CACHE.get("https://www.country-files.com/cty/cty.plist",
|
||||
response = _URL_DATA_CACHE.get("https://www.country-files.com/cty/cty.plist",
|
||||
headers=HTTP_HEADERS)
|
||||
|
||||
if response.ok:
|
||||
@@ -167,7 +168,7 @@ class LookupHelper:
|
||||
|
||||
try:
|
||||
logging.info("Downloading dxcc.json...")
|
||||
response = URL_DATA_CACHE.get(
|
||||
response = _URL_DATA_CACHE.get(
|
||||
"https://raw.githubusercontent.com/k0swe/dxcc-json/refs/heads/main/dxcc.json",
|
||||
headers=HTTP_HEADERS)
|
||||
|
||||
@@ -515,7 +516,7 @@ class LookupHelper:
|
||||
|
||||
for lookup_call in calls_to_try:
|
||||
try:
|
||||
response = URL_DATA_CACHE.get(
|
||||
response = _URL_DATA_CACHE.get(
|
||||
self._qrz_base_url + "?s=" + session_key + "&callsign=" + urllib.parse.quote_plus(lookup_call),
|
||||
headers=HTTP_HEADERS, timeout=10)
|
||||
if response.ok:
|
||||
@@ -593,7 +594,7 @@ class LookupHelper:
|
||||
|
||||
for lookup_call in calls_to_try:
|
||||
try:
|
||||
response = URL_DATA_CACHE.get(
|
||||
response = _URL_DATA_CACHE.get(
|
||||
self._hamqth_base_url + "?id=" + session_id + "&callsign=" + urllib.parse.quote_plus(
|
||||
lookup_call) + "&prg=" + HAMQTH_PRG, headers=HTTP_HEADERS)
|
||||
if response.ok:
|
||||
|
||||
+17
-15
@@ -3,21 +3,23 @@ from datetime import timedelta
|
||||
|
||||
from requests_cache import CachedSession
|
||||
|
||||
# Cache for "semi-static" data retrieved from a URL. This is a layet of caching in addition to the normal caching of
|
||||
# spots, alerts and other data in the DataStore class. Its purpose is to avoid hitting remote endpoints frequently when
|
||||
# e.g. restarting Spothole many times during testing.
|
||||
_session = CachedSession("cache/semi_static_urls", expire_after=timedelta(days=1),
|
||||
from core.data_store import CACHE_DIR
|
||||
|
||||
|
||||
class URLDataCache(CachedSession):
|
||||
"""Cache for "semi-static" data retrieved from a URL. This is an extra layer of caching in addition to the normal
|
||||
caching of spots, alerts and other data in the DataStore class. Its purpose is to avoid hitting remote endpoints
|
||||
frequently when e.g. restarting Spothole many times during testing. It should only be used for data that isn't
|
||||
expected to change on a sub-daily basis, so never for spots & alerts. It also implements a lock to allow it to be
|
||||
used across multiple threads, though note that URL lookups will block each other this way, so it is still better to
|
||||
create one of these objects per thread if possible."""
|
||||
|
||||
_lock = threading.Lock()
|
||||
|
||||
def __init__(self, name):
|
||||
super().__init__(CACHE_DIR + "urls/" + name, expire_after=timedelta(days=1),
|
||||
allowable_codes=(200, 400, 401, 403, 404))
|
||||
_lock = threading.Lock()
|
||||
|
||||
|
||||
class _ThreadSafeSession:
|
||||
"""Wraps CachedSession with a lock to prevent concurrent SQLite access across threads. This allows a single object
|
||||
to be used freely across the application."""
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
with _lock:
|
||||
return _session.get(*args, **kwargs)
|
||||
|
||||
# Global object
|
||||
URL_DATA_CACHE = _ThreadSafeSession()
|
||||
with self._lock:
|
||||
return super().get(*args, **kwargs)
|
||||
Reference in New Issue
Block a user