[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

オープン 初心者向け
#739 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

まだ誰も着手していません。

評価

難易度
2/5
見積もり時間
1〜3時間
初心者へのやさしさ
85/100
issue の種類
バグ
明瞭さ
明確に書かれている
活発さ
活発
技術スタック
typescript
領域
backend, testing

調査の方向性

src/analytics/ttl-cache.tsと、src/analytics/ttl-cache.spec.tsにある読み込み中の無効化のケースを読みます。制御可能な読み込みを開始し、解決前に無効化してから、2回目の無効化をせずに別のloaderでgetOrLoadを呼び出して競合を再現します。その結果がキャッシュされないようにキャッシュを更新し、新しいloaderが実行され、その値が返されることをspecで証明します。

索引モデルが issue の本文から書いたものです。

説明

bug testing

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

  1. In a unit test: const c = new TtlCache<string>(60000); start getOrLoad with a loader you control; call c.invalidate(); resolve the loader with 'stale'.
  2. Without a second invalidate(), call c.getOrLoad(loaderReturning('fresh')).
  3. It resolves to 'stale' and the second loader is never invoked. Expected: 'fresh'.
  4. End to end: issue GET /analytics/platform on 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 in invalidate(); capture its value when the load starts and only assign this.entry if 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 getOrLoad with a different loader and assert the second loader ran and its value was returned — with no extra invalidate() call in between.
主要言語
TypeScript
スター
2
フォーク
90
平均マージ
31分
マージ済み PR(30日)
20

環境構築

  • Dockerfile または Docker Compose ファイルあり
  • プルリクエストのテンプレートなし
  • コントリビューションガイドなし

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

MergeFi/backend のほかの issue

MergeFi/backend の issue をすべて見る

似ている issue

TypeScript の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。