From 366b7c81f2ab772d4d8a311a9ad8e18e775ed1be Mon Sep 17 00:00:00 2001 From: Hideaki Terai Date: Mon, 25 May 2026 08:08:19 +0900 Subject: [PATCH] =?UTF-8?q?=E3=83=AC=E3=82=B8=E3=83=A5=E3=83=BC=E3=83=A0?= =?UTF-8?q?=E6=99=82=E3=81=AE=E3=83=90=E3=83=83=E3=82=AF=E3=83=95=E3=82=A3?= =?UTF-8?q?=E3=83=AB=E5=AE=9F=E8=A3=85:=20=E3=82=BF=E3=82=A4=E3=83=9E?= =?UTF-8?q?=E3=83=BC=E3=83=89=E3=83=AA=E3=83=95=E3=83=88=E3=81=A7=E3=82=B9?= =?UTF-8?q?=E3=83=AA=E3=83=BC=E3=83=97=E5=BE=A9=E5=B8=B0=E3=82=92=E6=A4=9C?= =?UTF-8?q?=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PCがスリープ状態からレジュームした際に5分以上経過していた場合、 バックフィルを自動発動するようにした。 タイマードリフト検出(30秒間隔のsetIntervalで実経過時間を計測)を用いており、 追加依存ライブラリなしでクロスプラットフォームに動作する。 また、既存の起動時バックフィルロジックを runBackfill() 関数に抽出し、 isBackfilling フラグにより起動時・レジューム時の二重実行を防止した。 Co-Authored-By: Claude Sonnet 4.6 --- lib/core.js | 92 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 60 insertions(+), 32 deletions(-) diff --git a/lib/core.js b/lib/core.js index 9978f7e..6aa4804 100644 --- a/lib/core.js +++ b/lib/core.js @@ -555,15 +555,20 @@ core.start = async (commander) => { await refreshSlackData(); setInterval(refreshSlackData, refreshIntervalMinutes * 60 * 1000); - // 起動時差分バックフィル: 前回停止からのギャップが閾値を超えた場合のみ実行 - if (sqliteDb && backfillGapSeconds !== null && backfillGapSeconds > BACKFILL_GAP_THRESHOLD) { - const lastTsMap = getLastSlackTsPerChannel(sqliteDb); - // 過去24時間以内に活動があったチャンネルのみ対象にすることで呼び出し数を抑制 - const cutoff = Date.now() / 1000 - 24 * 3600; - const channelIds = Object.keys(lastTsMap).filter(id => parseFloat(lastTsMap[id]) > cutoff); + let isBackfilling = false; + + const runBackfill = async (label) => { + if (isBackfilling || !sqliteDb) return; + isBackfilling = true; + try { + const lastTsMap = getLastSlackTsPerChannel(sqliteDb); + // 過去24時間以内に活動があったチャンネルのみ対象にすることで呼び出し数を抑制 + const cutoff = Date.now() / 1000 - 24 * 3600; + const channelIds = Object.keys(lastTsMap).filter(id => parseFloat(lastTsMap[id]) > cutoff); + + if (channelIds.length === 0) return; - if (channelIds.length > 0) { - process.stdout.write(`Backfilling messages for ${channelIds.length} channel(s) in background...\n`); + process.stdout.write(`${label} Backfilling messages for ${channelIds.length} channel(s) in background...\n`); let totalFetched = 0; let processedChannels = 0; let earliestTs = null; @@ -662,32 +667,55 @@ core.start = async (commander) => { processedChannels++; }; - // バックグラウンドで実行 (RTM の起動を待たせない) - (async () => { - const progressInterval = setInterval(() => { - const range = earliestTs - ? `${moment(earliestTs * 1000).format("YYYY-MM-DD HH:mm")} 〜 ${moment(latestTs * 1000).format("YYYY-MM-DD HH:mm")}` - : "no messages yet"; - process.stdout.write(`Backfill progress: ${processedChannels}/${channelIds.length} channels, time range: ${range}\n`); - }, 60 * 1000); - - for (let i = 0; i < channelIds.length; i += CONCURRENCY) { - await Promise.all(channelIds.slice(i, i + CONCURRENCY).map(fetchChannel)); - } + const progressInterval = setInterval(() => { + const range = earliestTs + ? `${moment(earliestTs * 1000).format("YYYY-MM-DD HH:mm")} 〜 ${moment(latestTs * 1000).format("YYYY-MM-DD HH:mm")}` + : "no messages yet"; + process.stdout.write(`Backfill progress: ${processedChannels}/${channelIds.length} channels, time range: ${range}\n`); + }, 60 * 1000); - clearInterval(progressInterval); - if (totalFetched > 0) { - process.stdout.write(`Backfill complete: ${totalFetched} message(s) fetched.\n`); - Object.entries(fetchedPerChannel) - .sort((a, b) => b[1] - a[1]) - .forEach(([chId, count]) => { - process.stdout.write(` ${resolveChannelLabelKey(chId)}: ${count} message(s)\n`); - }); - } else { - process.stdout.write("Backfill complete: no new messages.\n"); - } - })(); + for (let i = 0; i < channelIds.length; i += CONCURRENCY) { + await Promise.all(channelIds.slice(i, i + CONCURRENCY).map(fetchChannel)); + } + + clearInterval(progressInterval); + if (totalFetched > 0) { + process.stdout.write(`Backfill complete: ${totalFetched} message(s) fetched.\n`); + Object.entries(fetchedPerChannel) + .sort((a, b) => b[1] - a[1]) + .forEach(([chId, count]) => { + process.stdout.write(` ${resolveChannelLabelKey(chId)}: ${count} message(s)\n`); + }); + } else { + process.stdout.write("Backfill complete: no new messages.\n"); + } + } finally { + isBackfilling = false; } + }; + + // 起動時差分バックフィル: 前回停止からのギャップが閾値を超えた場合のみ実行 + if (sqliteDb && backfillGapSeconds !== null && backfillGapSeconds > BACKFILL_GAP_THRESHOLD) { + // バックグラウンドで実行 (RTM の起動を待たせない) + runBackfill("[Startup]").catch(() => {}); + } + + // レジューム検出: タイマードリフトでスリープからの復帰を検知し、5分以上経過していればバックフィル発動 + if (sqliteDb) { + const SLEEP_DETECT_INTERVAL_MS = 30 * 1000; + const RESUME_THRESHOLD_MS = BACKFILL_GAP_THRESHOLD * 1000; + let lastSleepCheckAt = Date.now(); + + setInterval(() => { + const now = Date.now(); + const elapsed = now - lastSleepCheckAt; + lastSleepCheckAt = now; + + if (elapsed > RESUME_THRESHOLD_MS) { + process.stdout.write("System resume detected. Running backfill...\n"); + runBackfill("[Resume]").catch(() => {}); + } + }, SLEEP_DETECT_INTERVAL_MS); } // complete