Cache the Dell token to disk so it survives restarts
The in-process token cache was lost on every app restart, forcing a new token request each time and tripping Dell's token-endpoint rate limit. Add an L2 file cache in the instance dir (keyed by a hash of the client id, so the id never lands on disk), mirroring warranty_sync.py. Once one token is obtained it is reused for ~1h across restarts and workers, avoiding the cooldown in normal use. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,17 +8,49 @@ Per-vendor config lives in settings (warranty_<name>_*), masked where secret,
|
|||||||
so nothing is hardcoded and the integration is toggled per site.
|
so nothing is hardcoded and the integration is toggled per site.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
from flask import current_app
|
||||||
|
|
||||||
from shopdb.api import db
|
from shopdb.api import db
|
||||||
from shopdb.core.models import Setting
|
from shopdb.core.models import Setting
|
||||||
|
|
||||||
# Process-wide Dell token cache. Dell rate-limits the token endpoint, so a fresh
|
# Two-level Dell token cache. Dell rate-limits the token endpoint, so a fresh
|
||||||
# request per refresh trips a 401 cooldown. Tokens live ~1h; cache + reuse.
|
# request per refresh (or per app restart) trips a 401 cooldown. Tokens live ~1h.
|
||||||
|
# L1 = process memory (fast); L2 = a file in the instance dir so the token
|
||||||
|
# survives restarts and is shared across workers - mirrors warranty_sync.py.
|
||||||
_dell_token_cache = {}
|
_dell_token_cache = {}
|
||||||
|
|
||||||
|
|
||||||
|
def _token_file(clientid):
|
||||||
|
# Per-client file, name keyed by a hash so the id never lands on disk.
|
||||||
|
tag = hashlib.sha256((clientid or '').encode()).hexdigest()[:16]
|
||||||
|
return os.path.join(current_app.instance_path, f'.dell_token_{tag}.json')
|
||||||
|
|
||||||
|
|
||||||
|
def _read_file_token(clientid):
|
||||||
|
try:
|
||||||
|
with open(_token_file(clientid)) as handle:
|
||||||
|
cached = json.load(handle)
|
||||||
|
if cached.get('expires_at', 0) > time.time() + 60:
|
||||||
|
return cached['token']
|
||||||
|
except (OSError, ValueError, KeyError):
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _write_file_token(clientid, token, expires_at):
|
||||||
|
try:
|
||||||
|
os.makedirs(current_app.instance_path, exist_ok=True)
|
||||||
|
with open(_token_file(clientid), 'w') as handle:
|
||||||
|
json.dump({'token': token, 'expires_at': expires_at}, handle)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
# Dell TechDirect defaults, overridable via settings if Dell changes endpoints.
|
# Dell TechDirect defaults, overridable via settings if Dell changes endpoints.
|
||||||
# The asset-entitlements service lives under sbil/eapi (device.warranty path 404s
|
# The asset-entitlements service lives under sbil/eapi (device.warranty path 404s
|
||||||
# with "Service Not Found" for this account). Verified against the live API.
|
# with "Service Not Found" for this account). Verified against the live API.
|
||||||
@@ -82,10 +114,15 @@ class DellProvider(WarrantyProvider):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def _get_token(self, config):
|
def _get_token(self, config):
|
||||||
# Reuse a cached token while valid - Dell rate-limits the token endpoint.
|
clientid = config['clientid']
|
||||||
cached = _dell_token_cache.get(config['clientid'])
|
# L1: process memory.
|
||||||
|
cached = _dell_token_cache.get(clientid)
|
||||||
if cached and cached['expires_at'] > time.time() + 60:
|
if cached and cached['expires_at'] > time.time() + 60:
|
||||||
return cached['token']
|
return cached['token']
|
||||||
|
# L2: file cache (survives restarts / shared across workers).
|
||||||
|
file_token = _read_file_token(clientid)
|
||||||
|
if file_token:
|
||||||
|
return file_token
|
||||||
|
|
||||||
# Dell expects the client id/secret as HTTP Basic auth, grant type in the
|
# Dell expects the client id/secret as HTTP Basic auth, grant type in the
|
||||||
# body. (Passing them in the body trips the token endpoint's rate limiter.)
|
# body. (Passing them in the body trips the token endpoint's rate limiter.)
|
||||||
@@ -111,9 +148,9 @@ class DellProvider(WarrantyProvider):
|
|||||||
raise WarrantyLookupError(f'Dell auth failed: {exc}')
|
raise WarrantyLookupError(f'Dell auth failed: {exc}')
|
||||||
if not token:
|
if not token:
|
||||||
raise WarrantyLookupError('Dell auth returned no access_token')
|
raise WarrantyLookupError('Dell auth returned no access_token')
|
||||||
_dell_token_cache[config['clientid']] = {
|
expires_at = time.time() + expires_in - 60
|
||||||
'token': token, 'expires_at': time.time() + expires_in - 60,
|
_dell_token_cache[clientid] = {'token': token, 'expires_at': expires_at}
|
||||||
}
|
_write_file_token(clientid, token, expires_at)
|
||||||
return token
|
return token
|
||||||
|
|
||||||
def _fetch(self, config, token, servicetag):
|
def _fetch(self, config, token, servicetag):
|
||||||
|
|||||||
Reference in New Issue
Block a user