0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

外部HTML解析が3件連続unknownなら成功扱いにしない

0
Posted at

TL;DR

外部HTML解析で unknown が返ったとき、1件や2件なら既存キャッシュを保持して続行する。ただし3件連続したら、ページ構造変更や通信異常の可能性が高いので処理を失敗扱いにする。

const MAX_CONSECUTIVE_UNKNOWN = 3;

if (result?.status === 'unknown') {
  unknownStreak += 1;
  if (unknownStreak >= MAX_CONSECUTIVE_UNKNOWN) {
    throw new Error(
      `Catalog results were indeterminate for ${MAX_CONSECUTIVE_UNKNOWN} consecutive series; retry later`
    );
  }
  if (prevCache[series.key] == null) {
    newCache[series.key] = { ...result, checkedAt: Date.now() };
  }
  continue;
}

対象リポジトリ: harness17/kindle-series-sale-tracker

起きたこと

Chrome拡張で、Amazonの商品ページをバックグラウンドから解析し、シリーズごとの続刊・セール状態をキャッシュしている。

外部HTML解析では、次のように unknown が返ることがある。

  • ページ構造が一時的に変わった
  • ネットワークが不安定だった
  • 検索結果が一時的に期待した形で返らなかった

最初は、unknown のとき既存キャッシュを保持するようにした。これで、1件の解析失敗で「続刊あり」や「セール中」の確定情報を消さずに済む。

しかし、全件が unknown になるような異常まで成功扱いにすると別の問題が起きる。バックグラウンド処理が「正常に一巡した」と記録され、次回実行が設定間隔まで遅れてしまう。

原因

unknown は「結果が得られなかった」状態であり、正常結果ではない。ただし、1件だけ unknown になった時点で処理全体を止めると、外部サイトの一時的な揺れに弱くなる。

必要だったのは、次の2つを分けることだった。

状況 扱い
1件または2件の unknown 既存キャッシュを保持して続行
3件連続の unknown 処理全体を失敗扱いにして再試行

このために unknownStreak を持たせた。

修正

background側では、chunk処理の中で unknownStreak をインクリメントする。成功結果が取れたら0に戻す。

let unknownStreak = Number(initialUnknownStreak) || 0;

for (const series of chunk) {
  let result;
  try {
    result = await card.probeSeries(catalog, series);
  } catch (error) {
    failedCount += 1;
    unknownStreak = 0;
    continue;
  }

  if (result?.status === 'unknown') {
    unknownStreak += 1;
    if (unknownStreak >= MAX_CONSECUTIVE_UNKNOWN) {
      throw new Error(
        `Catalog results were indeterminate for ${MAX_CONSECUTIVE_UNKNOWN} consecutive series; retry later`
      );
    }
    if (prevCache[series.key] == null) {
      newCache[series.key] = { ...result, checkedAt: Date.now() };
    }
    continue;
  }

  unknownStreak = 0;
  newCache[series.key] = { ...result, checkedAt: Date.now() };
}

ポイントは3つ。

  • unknown を既存キャッシュの上書きに使わない(prevCache[series.key] == null のガード)
  • 連続数としては数える(unknownStreak += 1
  • 3件連続で閾値に達したら処理を止める(throw

成功時刻を更新しない

3件連続で unknown になった場合は throw する。runBackgroundProbeOnce() 側では catch で実行状態を failed にし、その後の markBgProbeCompleted() には進まない。

try {
  // chunkを順に処理する
} catch (error) {
  const failedEntry = {
    ...runState,
    status: 'failed',
    finishedAt: Date.now(),
    error: String(error?.message || error).slice(0, 200),
  };
  await storageSet({ [BG_PROBE_RUN_STATE_KEY]: failedEntry });
  await appendProbeHistory(failedEntry);
  throw error;
}

await markBgProbeCompleted(runState);

markBgProbeCompleted() だけが最終成功時刻を保存する。

async function markBgProbeCompleted(runState) {
  const finishedAt = Date.now();
  const entry = {
    ...runState,
    status: 'completed',
    finishedAt,
  };
  await storageSet({
    [BG_PROBE_LAST_RUN_KEY]: finishedAt,
    [BG_PROBE_RUN_STATE_KEY]: entry,
  });
  await appendProbeHistory(entry);
}

つまり、3件連続 unknown は「キャッシュを壊さない」だけでなく、「成功したことにしない」ための条件でもある。

確認ポイント

  • MAX_CONSECUTIVE_UNKNOWN3 になっている
  • 1件目・2件目の unknown では既存キャッシュを保持して続行する
  • 3件目の unknownthrow する
  • catch では BG_PROBE_RUN_STATE_KEYfailed にする
  • 失敗時は markBgProbeCompleted() に進まず、kstBgProbeLastRunAt を更新しない

この確認は verify-background-probe.mjs の担当範囲にしている。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?