プロセスが死んでも残る — いちばん賢くない方法で

5 分
目標MCP サーバー講座 5 編目。待ち人数が変数にあればプロセスと一緒に消える。ファイルに書くのだが、毎回すべてを書き直す。部分更新より遅い代わりに、書きかけの記録が残らない。

4 編目まで待ち人数はこう生きていた。

var waiting = 3;

プロセスが死ねば 3 に戻る。デスクの画面を抜き差しすれば行列が無かったことになる。

書き留める

const _statePath = 'desk-state.json';

int _load() {
  final f = File(_statePath);
  if (!f.existsSync()) return 3;
  final m = jsonDecode(f.readAsStringSync()) as Map<String, dynamic>;
  return (m['waiting'] as num).toInt();
}

void _save(int waiting) =>
    File(_statePath).writeAsStringSync(jsonEncode({'waiting': waiting}));

そして値が変わるときに呼ぶ。

waiting -= n;
_save(waiting);

なぜ全体を書き直すのか

値ひとつが変わるのにファイル全体を書き直す。非効率だ。ところがこの方式には性質がひとつある。

書きかけの記録が生まれない。 書き込みが起きたか起きなかったかのどちらかだ。途中で死ねば古いファイルがそのまま残る。

部分更新はそうではない。レコードひとつをその場で直していて死ねば どちらでもないファイル が残り、次の起動でパースが壊れる。そこからは復旧が仕事になる。

記録が数万件になればこの方式は使えない。そこが DB を使う場所だ。ただしそれまではこれがいちばん安全だ。

どの層に置くか

状態が置かれうる場所は三つある。

層 複数の画面が同じ値を見るか プロセスが死ぬと
画面の中 いいえ — 画面ごとに別の値 消える
サーバープロセス はい 消える
サーバーの外(ファイル・DB) はい 残る

4 編目までは二つ目だった。この編が三つ目へ移す。

決めなければ二つ目になる。 そしてそれが問題になる日はたいてい夜だ。

検証 — 実際にプロセスを殺す

永続は「保存した」を見てはいけない。切って繋ぎ直して読まねばならない。

rm -f desk-state.json
ask step5 '...{"name":"desk.admit","arguments":{"count":2}}}' >/dev/null   # プロセス 1
ask step5 '...{"name":"desk.state","arguments":{}}}' > captures/s5.txt      # プロセス 2
grep -q 'waiting.*1' captures/s5.txt || die "the count did not survive a restart"

ask は毎回新しいプロセスを立てる。最初の呼び出しが終われば、そのプロセスは死ぬ。

step5  admitted 2, process ended, a new process still reads waiting 1

サーバーのログにも残る。

course step5: up, state in desk-state.json (waiting=3)
course step5: up, state in desk-state.json (waiting=1)

二度目の起動が 1 から始まっている。

自分で動かす

dart run bin/step5.dart
bash verify.sh

持ち帰るもの

  1. _load / _save の二つ — ファイルが無いときの既定値も含めて
  2. 全体の書き直し — 書きかけの記録を作らない
  3. 再起動の往復検査 — 保存ログではなく、新しいプロセスの読み取りで確かめる

次の編

値が変わったことを画面に知らせる。問わせずに。

サンプルを実行する

makemind-academy/course_server/
git clone https://github.com/makemind-academy/course_server
cd course_server
dart pub get
dart run bin/step5.dart
GitHub で開く →