Experiment record
SwitchBot 5分Raw収集における毎時04分欠測の原因診断とScriptLock競合対策
毎時04分付近に集中していたSwitchBot 5分Raw欠測を調査し、API取得を保存Lockより前へ移して共有ScriptLock範囲を短縮した後、毎時04分34/34・全5分周期411/411・欠測0を確認したCompleted Experiment。
概要
AIEL-2026-0007では、毎時04分付近に欠測が集中していたSwitchBot 5分収集の原因を診断し、対策効果を検証しました。ここでRawとは、日次集計などの後段処理より前に保存される個々の5分観測行を指します。
同じGoogle Apps Scriptプロジェクト内には、今回関係する2つの処理経路がありました。1つはSwitchBotの5分collector、もう1つは別系統のガスデータrefresh処理です。両方が、同一Apps Scriptプロジェクト内の同時実行を排他的に制御する同じScriptLockを使用していました。保持された修正前Sourceでは、SwitchBotPowerがSwitchBot API取得より前にこのLockを取得しようとし、20秒以内に取得できない場合は観測を記録せずに終了し得る構造でした。一方、ガスデータrefresh処理には、より長い処理区間で同じLockを保持する経路があり、別処理同士でもLock競合が起こり得ました。
そこで、SwitchBot API取得を保存Lockより前へ移し、API取得時刻を観測Timestampとして固定し、Lockは短い書込み部分だけに保持する構造へ変更しました。ガスデータrefresh処理も、処理全体で共有Lockを保持せず、書込み部分だけに縮小しました。
修正後の効果判定は5分Rawだけで行いました。約34時間の検証で、毎時04分取得は34/34 = 100%、期待5分周期は411/411でした。欠測と7分超間隔はいずれも0件でした。
重複観測は1件だけで、直前観測から約38.5秒後に発生しました。既存の重複防御により後の観測が二重計上されることはなく、異常間隔の横断チェックでも再発傾向は確認されませんでした。
実験の背景
修正前のSwitchBot経路は概念的に次の順序でした。
定期起動 → 共有ScriptLock取得 → SwitchBot API取得 → Raw書込み
同じApps Scriptプロジェクト内の別系統のガスデータrefresh処理も同じ共有Lockを使っていたため、そちらの長いLock保持とSwitchBotの定期実行が重なる可能性がありました。SwitchBot側が20秒以内に共有Lockを取得できない場合、API取得前に実行が終了し得ます。この場合、後から保存できる観測値自体が存在しないため、Rawには無記録欠測が残ります。
定期collectorでは、外部観測と永続化を分離できる場合、保存競合が観測失敗に直結しない設計が有利です。外部観測を先に取得して取得時刻を保持すれば、その後の保存競合を観測取得とは別の問題として扱えます。
このfailure mechanism自体はSourceから直接確認できます。一方、「毎時04分に集中した実際の欠測の主要原因が共有ScriptLock競合だった」という因果帰属は、各欠測時点の直接Lock traceが残っていないため、INFERRED / HIGH confidenceとします。Source上のfailure mechanismと、Lock範囲変更後に対象パターンが消失した事実を組み合わせた推論です。
実験方法
- 修正前Apps Scriptの制御フローを確認し、観測前Lockとearly returnによる欠測経路を特定する。
- SwitchBot API取得をLock外へ移し、取得時刻を観測時刻として固定し、Lockを保存部分だけへ縮小する。同じLockを使う別系統のガスデータrefresh処理も、Lock範囲を書込み部分だけへ縮小する。
- 既存5分Rawを書き換えず、そのまま収集を継続する。
- 修正後Rawだけを用いて、期待5分周期、毎時04分取得、欠測、重複間隔、7分超間隔を確認する。
完了判定には日次集計を使用していません。今回の検証対象は、collector変更後に一次の5分観測そのものが欠けずに保存されているかどうかだからです。
実験ログ
修正前の制御フロー
保持されたSwitchBotPowerロジックはSwitchBot API取得前にScriptLockを取得していました。20秒以内にLockを取得できない場合、Raw観測を残さず実行終了し得る構造でした。同じApps Scriptプロジェクト内の別系統のガスデータrefresh処理も、より長い区間で同じ共有Lockを使用していました。
collector変更
修正後のSwitchBot経路は次の順序になりました。
定期起動 → SwitchBot API取得+取得Timestamp → 短時間の保存Lock → Raw書込み
ガスデータrefresh処理も、同じ共有Lockを処理全体で保持する構造をやめ、書込み部分だけに限定しました。
修正後Raw検証
| 確認項目 | 結果 |
|---|---|
| 毎時04分の期待観測 | 34 |
| 毎時04分の実観測 | 34 |
| 毎時04分取得成功率 | 100% |
| 期待5分周期 | 411 |
| 実観測5分周期 | 411 |
| 欠測 | 0 |
| 7分超間隔 | 0 |
| 重複観測 | 単発1件 |
重複1件は直前観測から約38.5秒後に発生しました。既存の重複防御により二重計上には至らず、反復パターンも確認されませんでした。
結論
修正前Sourceには、共有ScriptLock競合によってSwitchBot API取得前に定期実行が終了し、Raw観測を残さないfailure mechanismが確認されました。修正版ではAPI取得をこのLock依存から外し、取得時刻を観測時刻として保持し、同じLockを使用していた2つの処理経路の共有Lock範囲を短縮しました。
修正後約34時間のRawでは、毎時04分34/34、全5分周期411/411、欠測0、7分超間隔0でした。重複は既存防御で二重計上を防げた単発1件だけでした。
確認済みfailure mechanismと修正前後の挙動が一致するため、長時間保持された共有ScriptLockとの競合を、対象となった毎時04分欠測の主要原因と判断します。ただし、各過去欠測について直接Lock traceが残っていないため、この因果帰属は直接観測ではなくINFERREDです。一方、今回のfailure patternに対して修正後Rawで再発しなかったことはVERIFIEDです。
証拠区分のまとめ
このExperimentで得られた情報のEvidence Stateは次のとおりです。
各Evidence Stateの意味はEvidence State(証拠状態)を参照してください。
| 記録内容 | Evidence State | 根拠 |
|---|---|---|
修正前SwitchBot経路はScriptLock timeoutでAPI取得前に終了しRaw観測を残さない可能性があった | VERIFIED | 保持Source確認 |
| 同じApps Scriptプロジェクト内の別系統ガスデータrefresh処理が、より長い処理区間で同じ共有Lockを使用していた | VERIFIED | 保持Source確認 |
| 修正版SwitchBot経路はAPI取得を保存Lockより前に行い、取得時刻を観測時刻として使用する | VERIFIED | 保持された修正版Source |
| ガスデータrefresh処理の共有Lock範囲は書込み部分だけへ縮小された | VERIFIED | 保持された修正版Source |
| 修正後は毎時04分34/34、全5分周期411/411、欠測0、7分超間隔0だった | VERIFIED | 保持Raw |
| 約38.5秒後の重複1件があり、既存防御で二重計上を防げた | OBSERVED | 保持Raw |
長時間保持された共有ScriptLock競合が毎時04分欠測の主要原因だった | INFERRED | failure mechanism確認 + 修正前後Raw挙動 |
運用上の示唆
定期Apps Script collectorでは、外部観測と保存処理を分離できる場合、保存用Lockの取得成否が外部観測の実行可否を決めないようにする方が堅牢です。外部観測を先に取得・Timestamp化し、その後に短時間の保存Lockを使うことで、保存競合が無記録欠測へ変換されるリスクを下げられます。
この結果は今回検証したarchitectureとfailure patternに限定されます。今後のすべての欠測がLock起因だと一般化するものではありません。
機械可読な実験記録
このExperimentの機械可読な正本記録をJSONとして公開しています。
AIEL-2026-0007 experiment.json
関連実験
AIEL-2026-0001では、SwitchBot OpenAPIのstale状態を調べるため独立した5分Google Apps Script収集経路を利用しました。AIEL-2026-0007が対象とするのは別レイヤーで、観測と保存Lockの結合によるcollector側欠測です。