mirror of
https://github.com/NomaDamas/k-skill.git
synced 2026-06-24 02:04:11 +00:00
Prevent degraded stock search outages from sticking in cache
Reviewer feedback showed that partial KRX market failures could be cached as full search answers, masking recovery on the next identical request. This change adds a regression that fails first, skips route-level caching for degraded search payloads, and keeps the trade-info empty-snapshot contract documented alongside the partial-failure response semantics. Constraint: Existing PR #124 already targets dev and must remain the follow-up lane for issue #99 Constraint: Proxy behavior must stay read-only and dependency-free Rejected: Cache degraded search payloads for a short TTL | still risks transient false negatives during the TTL window Rejected: Broaden trade-info fallback behavior | empty snapshots should stay explicit not_found results Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep degraded search responses out of the long-lived route cache unless a future design adds explicit revalidation semantics Tested: npm test --workspace k-skill-proxy; node --test scripts/skill-docs.test.js; npm run ci; explicit buildServer degraded-search recovery repro Not-tested: Live KRX production endpoints from this branch
This commit is contained in:
parent
4903256f27
commit
8d6896d1cc
4 changed files with 110 additions and 3 deletions
|
|
@ -73,7 +73,8 @@ curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/korean-stock/trade-info'
|
|||
|
||||
- 잘못된 `market`, `code`, `bas_dd` 형식은 400
|
||||
- proxy 서버에 `KRX_API_KEY` 가 없으면 503
|
||||
- upstream KRX 오류는 502
|
||||
- 검색 중 일부 시장 upstream 이 실패하면 200 이지만 `upstream.degraded=true` / `failed_markets` 가 함께 온다.
|
||||
- 모든 요청 시장에서 upstream KRX 조회가 실패하면 502
|
||||
- 기준일에 종목을 찾지 못하면 404 `not_found`
|
||||
|
||||
## 참고 링크
|
||||
|
|
|
|||
|
|
@ -186,7 +186,8 @@ curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/korean-stock/trade-info'
|
|||
|
||||
- `q`, `market`, `code`, `bas_dd` 형식이 잘못되면 400 응답
|
||||
- 프록시 서버에 `KRX_API_KEY` 가 없으면 503 응답
|
||||
- upstream KRX 응답 오류면 502 응답
|
||||
- 검색 중 일부 시장 upstream 이 실패하면 200 응답이지만 `upstream.degraded=true` 와 `failed_markets` 를 함께 반환할 수 있다.
|
||||
- 모든 요청 시장에서 upstream KRX 조회가 실패하면 502 응답
|
||||
- 해당 기준일/시장에 종목이 없으면 404 `not_found`
|
||||
|
||||
## Done when
|
||||
|
|
|
|||
|
|
@ -2061,7 +2061,9 @@ function buildServer({ env = process.env, provider = null, now = () => new Date(
|
|||
payload.upstream = result.upstream;
|
||||
}
|
||||
|
||||
cache.set(cacheKey, payload, config.cacheTtlMs);
|
||||
if (!result.upstream?.degraded) {
|
||||
cache.set(cacheKey, payload, config.cacheTtlMs);
|
||||
}
|
||||
return payload;
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -264,6 +264,109 @@ test("korean stock search surfaces degraded upstream metadata when another marke
|
|||
assert.ok(fetchCalls.every((entry) => entry.url.startsWith("https://data-dbg.krx.co.kr/")));
|
||||
});
|
||||
|
||||
test("korean stock search does not cache degraded responses and retries a recovered market", async (t) => {
|
||||
const originalFetch = global.fetch;
|
||||
const fetchCalls = [];
|
||||
let kosdaqAttempts = 0;
|
||||
|
||||
global.fetch = async (url, options = {}) => {
|
||||
const text = String(url);
|
||||
fetchCalls.push({ url: text, headers: options.headers });
|
||||
|
||||
if (text.includes("stk_isu_base_info") || text.includes("knx_isu_base_info")) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
OutBlock_1: []
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json;charset=UTF-8" }
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (text.includes("ksq_isu_base_info")) {
|
||||
kosdaqAttempts += 1;
|
||||
|
||||
if (kosdaqAttempts === 1) {
|
||||
return new Response("boom", {
|
||||
status: 500,
|
||||
statusText: "Internal Server Error"
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
OutBlock_1: [
|
||||
{
|
||||
ISU_CD: "KR7196170005",
|
||||
ISU_SRT_CD: "196170",
|
||||
ISU_NM: "알테오젠",
|
||||
ISU_ABBRV: "알테오젠",
|
||||
ISU_ENG_NM: "Alteogen",
|
||||
LIST_DD: "20140509",
|
||||
MKT_TP_NM: "KOSDAQ",
|
||||
SECUGRP_NM: "주권",
|
||||
SECT_TP_NM: "제약",
|
||||
KIND_STKCERT_TP_NM: "보통주",
|
||||
PARVAL: "500",
|
||||
LIST_SHRS: "53470829"
|
||||
}
|
||||
]
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json;charset=UTF-8" }
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(`unexpected URL: ${url}`);
|
||||
};
|
||||
|
||||
const app = buildServer({
|
||||
env: {
|
||||
KRX_API_KEY: "krx-key",
|
||||
KSKILL_PROXY_CACHE_TTL_MS: "60000"
|
||||
}
|
||||
});
|
||||
|
||||
t.after(async () => {
|
||||
global.fetch = originalFetch;
|
||||
await app.close();
|
||||
});
|
||||
|
||||
const first = await app.inject({
|
||||
method: "GET",
|
||||
url: "/v1/korean-stock/search?q=%EC%95%8C%ED%85%8C%EC%98%A4%EC%A0%A0&bas_dd=20260408"
|
||||
});
|
||||
const second = await app.inject({
|
||||
method: "GET",
|
||||
url: "/v1/korean-stock/search?q=%EC%95%8C%ED%85%8C%EC%98%A4%EC%A0%A0&bas_dd=20260408"
|
||||
});
|
||||
|
||||
assert.equal(first.statusCode, 200);
|
||||
assert.equal(first.json().items.length, 0);
|
||||
assert.equal(first.json().proxy.cache.hit, false);
|
||||
assert.equal(first.json().upstream.degraded, true);
|
||||
assert.deepEqual(first.json().upstream.failed_markets, [
|
||||
{
|
||||
market: "KOSDAQ",
|
||||
code: "upstream_error",
|
||||
status_code: 502,
|
||||
message: "KRX API HTTP 오류 (status: 500): Internal Server Error"
|
||||
}
|
||||
]);
|
||||
|
||||
assert.equal(second.statusCode, 200);
|
||||
assert.equal(second.json().proxy.cache.hit, false);
|
||||
assert.equal(second.json().items.length, 1);
|
||||
assert.equal(second.json().items[0].market, "KOSDAQ");
|
||||
assert.equal(second.json().items[0].code, "196170");
|
||||
assert.equal(kosdaqAttempts, 2);
|
||||
assert.equal(fetchCalls.length, 4);
|
||||
});
|
||||
|
||||
test("korean stock search reuses per-market base snapshots across different queries for the same date", async (t) => {
|
||||
const originalFetch = global.fetch;
|
||||
const fetchCalls = [];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue