Deliver BAT-005: KIS token Redis cache (REQ-016, minor-change)
Port the trex-orbit Redis token-cache pattern so the KIS access token
survives daemon restarts and is shared across processes, avoiding KIS
token-issuance rate limits. KISBroker._ensure_token now resolves L1
in-memory -> L2 cache -> issue, storing issued tokens back to the cache.
- adapters/broker/token_cache.py: TokenCache protocol + RedisTokenCache
(key trex:broker:kis:{account_ref}:token, token+expiry+app_key suffix
JSON, ex=ttl, best-effort with graceful fallback) + InMemoryTokenCache.
- kis.py: optional token_cache injection.
- runtime config/daemon: TREX_KIS_REDIS_URL wires a RedisTokenCache.
- pyproject: redis>=5.
Verification: 96 passed, ruff clean — cache hit avoids issuance, cache is
shared across instances (restart/multi-process survival), backend errors
fall back to issuance (no regression), and app_key-suffix mismatch
invalidates a stale cache. Tested deterministically with an in-memory
double (no Redis server required). Processed via the minor-change fast
path: REQ-016 -> Implemented, BAT-005 (batch-lite) -> release-candidate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -40,6 +40,7 @@ class KISBroker:
|
||||
base_url: str,
|
||||
timeout: float = 5.0,
|
||||
allow_live: bool = False,
|
||||
token_cache=None,
|
||||
) -> None:
|
||||
self._account_ref = account_ref
|
||||
self._app_key = app_key
|
||||
@@ -48,6 +49,7 @@ class KISBroker:
|
||||
self._timeout = timeout
|
||||
self._token: Optional[str] = None
|
||||
self._token_exp: datetime = datetime.now(tz=timezone.utc)
|
||||
self._token_cache = token_cache # optional TokenCache (L2, REQ-016)
|
||||
is_mock = any(h in self._base_url for h in _MOCK_HINTS)
|
||||
if not is_mock and not allow_live:
|
||||
raise RuntimeError(
|
||||
@@ -61,8 +63,16 @@ class KISBroker:
|
||||
|
||||
# --- auth ---
|
||||
def _ensure_token(self) -> str:
|
||||
# L1: in-memory
|
||||
if self._token and datetime.now(tz=timezone.utc) < self._token_exp:
|
||||
return self._token
|
||||
# L2: shared/persistent cache (Redis) — survives restart / multi-process (REQ-016)
|
||||
if self._token_cache is not None:
|
||||
cached = self._token_cache.load(self._account_ref, self._app_key)
|
||||
if cached is not None:
|
||||
self._token, self._token_exp = cached
|
||||
return self._token
|
||||
# L3: issue from KIS
|
||||
resp = httpx.post(
|
||||
f"{self._base_url}/oauth2/tokenP",
|
||||
json={
|
||||
@@ -77,6 +87,8 @@ class KISBroker:
|
||||
self._token = data["access_token"]
|
||||
ttl = int(data.get("expires_in", 3600))
|
||||
self._token_exp = datetime.now(tz=timezone.utc) + timedelta(seconds=max(60, ttl - 300))
|
||||
if self._token_cache is not None:
|
||||
self._token_cache.store(self._account_ref, self._app_key, self._token, self._token_exp)
|
||||
return self._token
|
||||
|
||||
def _headers(self, tr_id: str) -> dict[str, str]:
|
||||
|
||||
Reference in New Issue
Block a user