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:
@@ -0,0 +1,88 @@
|
||||
"""KIS token cache tests (REQ-016).
|
||||
|
||||
Uses InMemoryTokenCache as a deterministic double + a stubbed token endpoint
|
||||
(httpx.post monkeypatched) so no real Redis server or network is needed.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from trex_core.adapters.broker.kis import KISBroker
|
||||
from trex_core.adapters.broker.token_cache import InMemoryTokenCache
|
||||
|
||||
_MOCK_URL = "https://openapivts.koreainvestment.com:29443"
|
||||
|
||||
|
||||
class _TokenResp:
|
||||
status_code = 200
|
||||
|
||||
def __init__(self, token: str) -> None:
|
||||
self._token = token
|
||||
|
||||
def raise_for_status(self) -> None:
|
||||
pass
|
||||
|
||||
def json(self) -> dict:
|
||||
return {"access_token": self._token, "expires_in": 86400}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def count_token_calls(monkeypatch):
|
||||
calls = {"n": 0}
|
||||
|
||||
def fake_post(url, **kwargs):
|
||||
assert url.endswith("/oauth2/tokenP")
|
||||
calls["n"] += 1
|
||||
return _TokenResp(f"TOK-{calls['n']}")
|
||||
|
||||
monkeypatch.setattr(httpx, "post", fake_post)
|
||||
return calls
|
||||
|
||||
|
||||
def _broker(cache, app_key="appkey0001"):
|
||||
return KISBroker("12345678-01", app_key=app_key, app_secret="s",
|
||||
base_url=_MOCK_URL, token_cache=cache)
|
||||
|
||||
|
||||
def test_cache_hit_avoids_issuance(count_token_calls):
|
||||
cache = InMemoryTokenCache()
|
||||
b = _broker(cache)
|
||||
t1 = b._ensure_token() # L3 issue + store
|
||||
assert count_token_calls["n"] == 1
|
||||
b._token = None # drop L1 to force L2 lookup
|
||||
t2 = b._ensure_token() # L2 cache hit — no new issuance
|
||||
assert t2 == t1 and count_token_calls["n"] == 1
|
||||
|
||||
|
||||
def test_shared_cache_across_instances(count_token_calls):
|
||||
cache = InMemoryTokenCache()
|
||||
a = _broker(cache)
|
||||
tok = a._ensure_token() # instance A issues + stores
|
||||
assert count_token_calls["n"] == 1
|
||||
b = _broker(cache) # instance B (e.g. after restart) shares cache
|
||||
assert b._ensure_token() == tok and count_token_calls["n"] == 1
|
||||
|
||||
|
||||
def test_graceful_fallback_when_cache_errors(count_token_calls):
|
||||
# RedisTokenCache swallows backend errors and returns None on miss; this
|
||||
# double models that post-swallow behaviour, so the broker still issues.
|
||||
class SilentCache:
|
||||
def load(self, *a, **k):
|
||||
return None
|
||||
|
||||
def store(self, *a, **k):
|
||||
return None
|
||||
|
||||
b = _broker(SilentCache())
|
||||
assert b._ensure_token() == "TOK-1" and count_token_calls["n"] == 1
|
||||
|
||||
|
||||
def test_app_key_suffix_mismatch_ignores_cache(count_token_calls):
|
||||
cache = InMemoryTokenCache()
|
||||
a = _broker(cache, app_key="AAAA1111")
|
||||
a._ensure_token() # cached under suffix 1111
|
||||
assert count_token_calls["n"] == 1
|
||||
b = _broker(cache, app_key="BBBB2222") # rotated credential, suffix 2222
|
||||
b._ensure_token() # must NOT reuse stale cache -> re-issue
|
||||
assert count_token_calls["n"] == 2
|
||||
Reference in New Issue
Block a user