Raspberry PiでBoatRaceのpaper runnerをsystemd timerから起動したところ、毎分失敗が続いた。journalには flock: failed to execute .../.venv/bin/python: No such file or directory と表示され、手動確認でも venv/bin/python: No such file or directory になった。
原因はPythonの依存パッケージではなく、実在する仮想環境と、systemd unitが呼ぶパスの不一致だった。さらに、Piへコードを置いた場所と手順書の cd 先にも食い違いがあった。
journalctlには修正前の失敗も残る
journalctl -n 100 の出力には、修正前の19時台の失敗が残っていた。ログ全体を同じ状態の繰り返しと見なさず、時刻を追うと、手動修正後の20:00:24に新しい起動が始まっていた。
最初に見えていたエラーは次の2種類だった。systemd側は status=69/UNAVAILABLE で終了していた。
bash: venv/bin/python: No such file or directory
flock: failed to execute /home/<user>/work/BoatRace/.venv/bin/python: No such file or directory
前者は ~/BoatRace でコマンドを実行したとき、後者はsystemd unitが起動したときのエラーである。どちらもファイルがない点は同じだが、参照している場所と名前が違う。
installerはvenv、systemdは.venvを参照
Pi向けインストーラーはプロジェクト直下に venv を作り、検査コマンドも venv/bin/python で実行していた。一方、runnerと結果突合のsystemd unitテンプレートは .venv/bin/python を指定していた。
installer: @PROJECT_DIR@/venv/bin/python
systemd: @PROJECT_DIR@/.venv/bin/python
そのため、依存関係のインストールが済んでいても、timerから起動するたびにsystemdは存在しない .venv を探していた。
手動実行のエラーには、checkout先の混同も重なっていた。Piのファイルは ~/work/BoatRace に置かれているのに、一部の手順は cd ~/BoatRace と案内していた。正しいcheckoutへ移動してから venv/bin/python を実行する必要があった。
既存のsystemd unitをその場で修正
Pi上でcheckout先を ~/work/BoatRace に移し、インストール済みの3つのunitにあるPythonパスを .venv から venv に変更した。ソースファイルの転送は行わず、Pi上のunitを直接修正した。
cd ~/work/BoatRace
sudo sed -i 's|/[.]venv/bin/python|/venv/bin/python|g' \
/etc/systemd/system/boatrace-raspberry-pi-runner.service \
/etc/systemd/system/boatrace-raspberry-pi-reconcile-today.service \
/etc/systemd/system/boatrace-raspberry-pi-reconcile-yesterday.service
sudo systemctl daemon-reload
sudo systemctl start boatrace-raspberry-pi-runner.service
journalctl -u boatrace-raspberry-pi-runner.service -n 100 --no-pager
3つのunitをまとめて直したのは、結果突合の2つも同じ .venv を参照していたためである。runnerだけ直すと、日次の精算サービスで同じ起動エラーが後から起きる。
journalでpaper runnerの完了を確認
修正後のログでは、PaperVoter (--live 未指定) と出てから開催日程を取得し、締切4分以内のレースを予測していた。20:06:00には recs=0, total_stake=¥0、直後に Deactivated successfully が記録された。
この実行は起動エラーではなく正常終了だった。条件に合うpaper推薦が0件だったため、賭け金も0円である。ログにXMLをHTML parserで読む XMLParsedAsHTMLWarning が繰り返し出たが、runnerは最後まで進み、systemd上も正常に終了していた。この警告は仮想環境パスの障害とは別に扱う。
再発を防ぐために残した変更
その場の修正とは別に、作業用リポジトリでは3つのunitテンプレートをインストーラーと同じ venv/bin/python に揃え、回帰テストを追加した。Pi向け手順書も ~/work/BoatRace に統一し、Mac側の .venv/ や venv/ をPiへrsyncしないようにした。
今回、Piではインストール済みunitを直接直した。リポジトリ側のテンプレート変更はその時点ではPiへ転送していないため、次にPi上でインストーラーを使ってunitを再生成するときは、修正版のテンプレートを先にPiへ同期しておく必要がある。
同じエラーを調べるときの確認順
- unitが実際に使う
ExecStartをsystemctl catで確認する。 - checkout先と仮想環境の実在パスを
pwdとtest -x venv/bin/pythonで確認する。 - installer、systemd unit、手順書の3か所で環境ディレクトリ名とcheckout先が一致しているか比べる。
daemon-reload後にサービスを起動し、journalの最新時刻と終了結果を見る。古い失敗ログだけで復旧可否を判断しない。
Raspberry PiのPython runnerで No such file or directory が出たときは、パッケージ不足を調べる前に、systemdが参照する実行ファイルのパスと、実際に作成されたvenvの名前を確認すると原因を絞り込みやすい。