[Regression on closed #463] TtlCache.invalidate() is still lost when it lands during an in-flight load, and the spec that covers it asserts nothing
メンテナーはふだん 1 日以内に返信
まだ誰も着手していません。
評価
- 難易度
- 2/5
- 見積もり時間
- 1〜3時間
- 初心者へのやさしさ
- 85/100
- issue の種類
- バグ
- 明瞭さ
- 明確に書かれている
- 活発さ
- 活発
- 技術スタック
- typescript
調査の方向性
src/analytics/ttl-cache.tsと、src/analytics/ttl-cache.spec.tsにある読み込み中の無効化のケースを読みます。制御可能な読み込みを開始し、解決前に無効化してから、2回目の無効化をせずに別のloaderでgetOrLoadを呼び出して競合を再現します。その結果がキャッシュされないようにキャッシュを更新し、新しいloaderが実行され、その値が返されることをspecで証明します。
索引モデルが issue の本文から書いたものです。
説明
Location — src/analytics/ttl-cache.ts:11 (invalidate) and src/analytics/ttl-cache.ts:15-33 (getOrLoad, specifically the write at line 26 this.entry = { data, expiresAt: Date.now() + this.ttlMs };). Consumer: src/analytics/analytics.service.ts:91 (invalidatePlatformSummary, the @OnEvent(ANALYTICS_PLATFORM_INVALIDATE_EVENT) handler).
Problem — The lost-invalidation race closed as #463 is present again. invalidate() clears this.entry but has no way to tell an already-running loader that its result is stale, and the loader unconditionally publishes its result into this.entry when it resolves:
invalidate(): void {
this.entry = null;
}
this.inflight = (async () => {
try {
const data = await loader();
this.entry = { data, expiresAt: Date.now() + this.ttlMs }; // overwrites the invalidation
return data;
} finally {
this.inflight = null;
}
})();
There is no generation counter, epoch, or "invalidated while loading" flag. So if a bounty is created or paid (which emits ANALYTICS_PLATFORM_INVALIDATE_EVENT) while loadPlatformSummary() is awaiting its three Postgres aggregates, the invalidation is silently discarded and the pre-write snapshot is cached for a full TTL (ANALYTICS_PLATFORM_SUMMARY_TTL_MS, default 60s).
Why it matters — The invalidation exists precisely so the public homepage numbers update immediately after a bounty is funded or paid. The race window is the duration of the aggregate query, which is the window in which bounty writes are most likely (the write that emits the event is concurrent with homepage traffic by definition). The result is GET /analytics/platform serving totalBounties / totalPaidOut that are provably behind committed data for up to a minute, with no way for a client to force a refresh.
The test that is supposed to cover this does not: src/analytics/ttl-cache.spec.ts:55 ('handles invalidate while load is in flight') invalidates during the load, then at line 73 calls cache.invalidate() a second time before the next getOrLoad, so the assertion that the loader runs again passes regardless of whether the first, in-flight invalidation had any effect. Its own comment ("Invalidate should have cleared entry, or subsequent call fetches if expired/invalidated") concedes the test does not distinguish the two. That vacuous test is why #463 could be closed with the bug intact.
Reproduction / failure path
- In a unit test:
const c = new TtlCache<string>(60000); startgetOrLoadwith a loader you control; callc.invalidate(); resolve the loader with'stale'. - Without a second
invalidate(), callc.getOrLoad(loaderReturning('fresh')). - It resolves to
'stale'and the second loader is never invoked. Expected:'fresh'. - End to end: issue
GET /analytics/platformon a cold cache and, while the query is in flight, create and pay a bounty. The next 60s of homepage responses omit it.
Suggested fix
- Add a monotonic
private generation = 0; increment it ininvalidate(); capture its value when the load starts and only assignthis.entryif the captured value still equals the current generation (otherwise return the data to the waiting callers but do not cache it). - Rewrite the spec so the in-flight case is asserted directly: invalidate during the load, resolve the loader, then call
getOrLoadwith a different loader and assert the second loader ran and its value was returned — with no extrainvalidate()call in between.
- 主要言語
- TypeScript
- スター
- 2
- フォーク
- 90
- 平均マージ
- 31分
- マージ済み PR(30日)
- 20
環境構築
- Dockerfile または Docker Compose ファイルあり
- プルリクエストのテンプレートなし
- コントリビューションガイドなし
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
MergeFi/backend のほかの issue
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 85/100
メンテナーはふだん 1 日以内に返信
-
bug security
難易度 2/5 1〜3時間 初心者へのやさしさ 74/100
メンテナーはふだん 1 日以内に返信
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
メンテナーはふだん 1 日以内に返信
-
bug security
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
メンテナーはふだん 1 日以内に返信
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 79/100
メンテナーはふだん 1 日以内に返信
MergeFi/backend の issue をすべて見る
似ている issue
-
Codex使用可 コード
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
ryuki-moro/Nicomaru#218 ·
メンテナーはふだん 3 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
sveltejs/svelte-eslint-parser#1008 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
silverbucket/inbox-rs#351 ·
メンテナーはふだん 1 日以内に返信
-
[Bug]: The H2 series' and the H3C's fan 3 is labelled "Chamber Fan", Bambu calls it the exhaust fanオープンarea:ui
難易度 2/5 1〜3時間 初心者へのやさしさ 88/100
メンテナーはふだん 1 日以内に返信
-
bug front P2
難易度 2/5 1〜3時間 初心者へのやさしさ 86/100
invoicerr-app/invoicerr#964 ·
メンテナーはふだん 1 日以内に返信