fix(configure): non-blocking /api/updates + periodic update poll for the always-on service#245
Merged
Merged
Conversation
…ng the UI `GET /api/updates` ran `pip list --outdated --format=json` synchronously on every request. On a slow or unreachable index that subprocess takes up to its 60s timeout, and the configure UI fetches the endpoint more than once per page load — so each `mureo configure` blocked the request thread repeatedly, dumped a full TimeoutExpired traceback to the console per attempt, and the browser gave up mid-response (BrokenPipeError). Fix: - version_check.py: add a non-blocking accessor `get_update_status()` that runs the slow `check_for_updates()` once in a daemon thread and caches the result (TTL: 6h ok / 10min error). The handler returns instantly; a cold cache yields `status="checking"`, which the frontend already treats as "nothing to show". Single-flight via `_refresh_in_progress` prevents the per-page-load thundering herd. All shared cache state is lock-guarded; the accessor returns a copy so a caller can't corrupt the cache. - Downgrade the expected timeout/OSError log from `logger.exception` (multi-line traceback) to a single `logger.warning` line. - handlers.py: `/api/updates` now calls `get_update_status()`. - Core `check_for_updates()` is unchanged. Tests: update the 3 handler route tests to patch `get_update_status`; add cache- layer tests (cold-start placeholder, caching, error TTL, stale refresh, and the concurrent single-flight invariant).
…rvice The non-blocking `/api/updates` cache is populated lazily — only when the UI is opened. For the always-on service (`mureo configure --serve` / launchd / systemd) nobody opens the UI, so the cache stays cold and the update badge never appears until a manual visit. Add a periodic poll that warms the cache on a wide cadence: - version_check.py: `start_periodic_update_check()` / `stop_periodic_update_check()` run a daemon thread that refreshes the cache immediately, then once per interval. Default 6h, overridable via `MUREO_UPDATE_CHECK_INTERVAL_SECONDS` (0/negative disables; non-finite values fall back to the default). The poll shares the lazy path's single-flight gate (`_refresh_if_idle`) so the two never double-run, and each launch owns its own stop Event so a stop/start race can't strand a poller. Threads are daemons; the shutdown join is best-effort. - server.py: `run_configure_wizard` starts the poll after the server is ready ONLY in `serve_forever` mode, and stops it in the `finally`. Short-lived interactive launches keep relying on the lazy on-open check. Tests: TestPeriodicUpdateCheck covers interval resolution (arg/env/default/ invalid), the 0=disabled switch, immediate cache warm, idempotent start, start→stop→start restart, and the single-flight skip. test_configure_serve.py gets an autouse fixture disabling the poll so serve tests never spawn real pip.
This was referenced Jun 13, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
mureo configureでpip list --outdated(この環境では60秒タイムアウト)がリクエスト毎に同期実行され、UI起動の度にブロック+トレースバック多発+BrokenPipe を起こしていた問題への対応。2コミットで構成。1) 非ブロッキング化(fix)
GET /api/updatesがpip list --outdated --format=jsonをリクエスト毎に同期実行。遅い/到達不能な index ではこのサブプロセスが最大60秒かかり、configure UI は1ページロードあたり複数回フェッチするため、毎回リクエストスレッドをブロック→TimeoutExpiredのフルトレースバックを連発→ブラウザが応答途中で切断 (BrokenPipeError)。version_check.py: 非ブロッキングアクセサget_update_status()を追加。遅いcheck_for_updates()をデーモンスレッドで1回だけ実行しキャッシュ(TTL: 成功6h / 失敗10min)。ハンドラは即返却、cold時はstatus="checking"(フロントは非okを無視)。_refresh_in_progressの single-flight で「1ページロードで複数pip」を防止。共有状態はロック保護、戻り値はコピー。logger.exception(多行トレースバック)→logger.warning(1行)に。handlers.py:/api/updatesをget_update_status()に切替。コアcheck_for_updates()は不変。2) 常時起動サービス向け定期ポーリング(feat)
非ブロッキングキャッシュはUIを開いたときしか温まらない。常時起動サービス(
--serve/ launchd / systemd)では誰もUIを開かないためキャッシュが cold のままで、更新バッジが手動アクセスまで出ない。version_check.py:start/stop_periodic_update_check()がデーモンスレッドで「起動直後に1回 → 以降 interval 毎」にキャッシュを温める。デフォルト6時間、MUREO_UPDATE_CHECK_INTERVAL_SECONDSで上書き(0/負で無効、非finiteはデフォルトにフォールバック)。lazy 経路と single-flight ゲート (_refresh_if_idle) を共有し二重実行を防止。各起動が自前の stop Event を持つので stop/start 競合でポーラーが取り残されない。daemon スレッドなので shutdown の join はベストエフォート。server.py:run_configure_wizardが serve_forever モードのみ、サーバ ready 後にポーリング開始・finallyで停止。短命な対話起動は従来の「UI起動時 lazy チェック」のまま。設計判断(更新マークのタイミング)
Test plan
pytest tests/test_web_version_check.py tests/test_web_handlers.py tests/test_web_server.py tests/test_configure_serve.py→ 234 passedruff/black --checkクリーンReview
code-reviewer を各コミット前に実施。指摘の thread-safety 項目を都度修正:
_refresh_threadのロック内書き込み / warm-path のコピー返し / 並行 single-flight テスト追加