ウォッチドッグのドキュメントを更新し、使用例を追加。型注釈とエラーハンドリングの改善を反映。

This commit is contained in:
misyaguziya
2025-10-09 17:01:31 +09:00
parent 569d8e3f76
commit 7255722b67
2 changed files with 164 additions and 12 deletions

View File

@@ -5,8 +5,76 @@
設計: 設計:
- class Watchdog(timeout: int = 60, interval: int = 20) - class Watchdog(timeout: int = 60, interval: int = 20)
- feed(): 最終フィード時刻を更新 - feed(): 最終フィード時刻を更新
- setCallback(callback): タイムアウト時に呼ぶコールバックを登録 - setCallback(callback): タイムアウト時に呼ぶコールバックを登録zero-arg を想定)
- start(): 現状は単純で、呼び出し側ループ中に start() を呼ぶかたち。実装は簡易(将来的にスレッド化推奨 - start(): 単一チェックを行い、`interval` 秒の sleep を行う(継続監視は呼び出し側ループまたはスレッド化)
注意: 注意:
- 現行実装は非常にシンプルで、長時間のブロッキングやスレッド運用の見直しが必要になり得る。 - 現行実装は非常にシンプルで、長時間のブロッキングやスレッド運用の見直しが必要になり得る。
変更点(実装に入れた改善):
- コールバック属性を初期化しておくことで AttributeError を防止
- コールバック呼び出し内の例外はウォッチドッグ本体に影響を与えないよう try/except で保護
- メソッドに型注釈と docstring を追加
短い使用例(ポーリング方式):
```py
import time
from models.watchdog.watchdog import Watchdog
def on_timeout():
print('watchdog timed out')
wd = Watchdog(timeout=5, interval=1)
wd.setCallback(on_timeout)
# 別スレッドにせず、単純なループでポーリングする例
while True:
wd.start() # ここで timeout をチェックし、必要なら callback を呼ぶ
# アプリケーションの他処理...
time.sleep(0.5)
# 正常時に feed を呼ぶ例
# wd.feed()
```
使用例(スレッド化ヘルパを用意するアプローチ):
```py
import time
from threading import Thread, Event
from models.watchdog.watchdog import Watchdog
stop_event = Event()
def run_watchdog(wd: Watchdog, stop_event: Event):
# シンプルなバックグラウンド実行ループ(安全な停止用フラグ付き)
while not stop_event.is_set():
wd.start()
wd = Watchdog(timeout=10, interval=1)
wd.setCallback(lambda: print('timed out'))
thread = Thread(target=run_watchdog, args=(wd, stop_event), daemon=True)
thread.start()
# 正常動作時
wd.feed()
time.sleep(2)
# 停止する場合は stop_event.set() を呼ぶ
stop_event.set()
thread.join()
```
拡張案(将来の改善):
- `start_in_thread()` / `stop()` を Watchdog に組み込む(内部で Thread と Event を管理して安全に停止できるようにする)
- コールバックに引数を渡せるようにするcontext 情報、呼び出し回数など)
- asyncio と相互運用できるバージョンasync/await ベース)を用意する
- ロギング統合(標準 logging を使って状態変化を記録)
- 単発one-shot/繰り返しの動作モード指定
簡易テスト済み:
- 基本的なコールバックの有効無効挙動をローカルで確認済みfeed 後は呼ばれず、タイムアウト状態で呼ばれる)。
注意事項:
- フル自動化CI での運用)を行う場合は、スレッド起動・停止のテストを追加することを推奨します。

View File

@@ -1,20 +1,104 @@
from typing import Callable from typing import Callable, Optional
import time import time
from threading import Thread, Event
class Watchdog: class Watchdog:
def __init__(self, timeout:int=60, interval:int=20): """A lightweight watchdog utility.
This class provides a minimal watchdog which records the last "feed"
timestamp and can invoke a user-supplied callback when the timeout
is exceeded. The design is intentionally simple: callers are expected
to either call `start()` periodically (e.g. from a loop) or extend the
class to run `start()` in a background thread.
Args:
timeout: seconds without feed after which the callback is invoked
interval: suggested sleep interval (seconds) for callers that poll
"""
def __init__(self, timeout: int = 60, interval: int = 20) -> None:
self.timeout = timeout self.timeout = timeout
self.interval = interval self.interval = interval
self.last_feed_time = time.time() self.last_feed_time = time.time()
self.callback: Optional[Callable[[], None]] = None
# Background thread control
self._thread: Optional[Thread] = None
self._stop_event: Optional[Event] = None
def feed(self): def feed(self) -> None:
"""Refresh the watchdog timer (set last feed time to now)."""
self.last_feed_time = time.time() self.last_feed_time = time.time()
def setCallback(self, callback): def setCallback(self, callback: Callable[[], None]) -> None:
"""Register a zero-argument callback invoked on timeout."""
self.callback = callback self.callback = callback
def start(self): def start(self) -> None:
if time.time() - self.last_feed_time > self.timeout: """Perform a single watchdog check and optionally sleep `interval` seconds.
if isinstance(self.callback, Callable):
The method checks if the duration since the last feed exceeds
`timeout`. If so and a callback is registered, the callback is called.
Note: `start()` does not run in the background by itself; callers
should call it repeatedly (or run it inside a thread) if continuous
monitoring is required.
"""
now = time.time()
if now - self.last_feed_time > self.timeout:
if callable(self.callback):
try:
self.callback() self.callback()
except Exception:
# Do not let callback exceptions propagate out of watchdog
import traceback
traceback.print_exc()
time.sleep(self.interval) time.sleep(self.interval)
def _run_loop(self) -> None:
"""Internal run loop used by `start_in_thread`.
It repeatedly calls `start()` until `_stop_event` is set. The
implementation relies on `start()` sleeping for `self.interval`.
"""
# Defensive: ensure stop_event exists
if self._stop_event is None:
return
while not self._stop_event.is_set():
self.start()
def start_in_thread(self, daemon: bool = True) -> None:
"""Start the watchdog in a background thread.
If the watchdog is already running, this is a no-op. The created
thread will repeatedly call `start()` until `stop()` is invoked.
Args:
daemon: if True, thread is a daemon thread (won't block process exit)
"""
if self._thread is not None and self._thread.is_alive():
return
self._stop_event = Event()
self._thread = Thread(target=self._run_loop, daemon=daemon)
self._thread.start()
def stop(self, timeout: Optional[float] = None) -> None:
"""Stop background thread started by `start_in_thread`.
If no background thread is running this is a no-op.
Args:
timeout: optional timeout to wait for thread join (seconds). If
None, join will block until the thread exits.
"""
if self._stop_event is None or self._thread is None:
return
# signal stop and wait for thread to finish
self._stop_event.set()
self._thread.join(timeout=timeout)
# cleanup
if self._thread.is_alive():
# thread did not stop within timeout; leave objects for another stop()
return
self._thread = None
self._stop_event = None