ファイルの置換と書込みの永続化
一時ファイルと名前の置換を使い、失敗した時点で残る内容を確かめます。
説明
ファイルへ書き込むと、プログラム、OS、記憶装置の間に未完了の処理が残る場合があります。読取りで新しい内容が見えることと、電源断後にも残ることは別です。
まず一時ファイルから設定を置き換え、名前と開いた参照の変化を確認します。その後、内容とディレクトリの永続化を別の状態として計算し、停止位置や容量不足によって残る内容を調べます。
1設定ファイルと開いた参照
一時ディレクトリに古い設定を用意し、読取り用に開いておきます。
from pathlib import Path; import tempfile, osファイル操作を準備; lab = tempfile.TemporaryDirectory(); folder = Path(lab.name)一時ディレクトリ; config = folder / "config.txt"; config.write_bytes(b"mode=old\n")古い設定を書く; old = config.open("rb"); print(old.read())開いたまま内容を読む先頭の9は書き込んだバイト数です。9バイトの古い設定です。oldはこのファイルを開いた参照を保持します。以後、同じ名前を別ファイルへ置き換えたときの動作も確認します。
2新しい内容を別ファイルへ書く
公開中のconfig.txtに直接上書きせず、同じディレクトリのnew.tmpへ新しい設定を書きます。
pending = folder / "new.tmp"; f = pending.open("wb")別ファイルを開く; f.write(b"mode=new\n"); f.flush()内容を書いてOSへ渡す; os.fsync(f.fileno()); f.close()内容を同期して閉じる; print(config.read_bytes(), pending.read_bytes())公開中と更新候補を比較9は書込み長です。config.txtは古い内容、new.tmpは新しい内容です。flushはPythonのバッファをOSへ渡し、fsyncはファイルの更新を記憶装置へ同期します。成功・失敗の戻り値を確認したうえで、次の置換へ進む必要があります。
3名前を一度に置き換える
同じファイルシステム上でnew.tmpをconfig.txtへ置き換えます。
os.replace(pending, config)名前を新しいファイルへ切り替える; print(config.read_bytes(), pending.exists())名前から見える内容; old.seek(0); print(old.read())以前の参照から読む途中の0はseekが戻したファイル位置です。名前から新しく開くとmode=newですが、置換前に開いたoldはmode=oldのままです。同一ファイルシステム内のrenameによる置換では、対象名が途中で存在しなくなる状態を避けられます。この名前の切替えだけで、電源断後の保存まで保証するわけではありません。
4内容と名前の保存を別々に表す
ここからは停止時の状態を計算します。保存済みの名前configはoldの内容を指し、新しい内容newはファイル同期を終えた状態とします。
durable_data = {"old": b"mode=old\n", "new": b"mode=new\n"}同期済みの内容; durable_names = {"config": "old"}; live_names = dict(durable_names)保存済みと実行中の名前; live_names["config"] = "new"実行中の名前を置換; print(durable_data[live_names["config"]], durable_data[durable_names["config"]])停止前後の参照を比較実行中に見える内容はnew、保存済みの名前から読むとoldです。これは、名前の変更がまだ保存されていない場合を表すモデルです。実際に未同期で停止した場合の結果はファイルシステム等に依存し、常に旧版へ戻ると決まっているわけではありません。
5ディレクトリも同期した後の状態
Linuxではファイルのfsyncだけで親ディレクトリの名前の更新が保存されるとは限りません。親ディレクトリのfsyncも成功した後の状態へ進めます。
durable_names = dict(live_names)名前の更新も保存済みにする; recovered = durable_data[durable_names["config"]]保存済みの参照から復旧; print(recovered)復旧後の内容保存済みの名前もnewを指すので、新しい内容を取り出せます。基本の順序は一時ファイルへ全量書込み、ファイル同期、rename、親ディレクトリ同期です。媒体やファイルシステムが同期要求を正しく実装することを前提とします。
6途中まで書けた場合
8バイトを書こうとして、最初のwriteが3バイトだけ受け付けた場合を計算します。残りを次のwriteで処理します。
payload = b"ABCDEFGH"; accepted = 3要求長と受理長; written = payload[:accepted]; remaining = payload[accepted:]完了分と残り; print(written, remaining)境界を確認; written += remaining; print(written == payload)残りを処理して照合writeの戻り値は要求した長さより短くなることがあります。最初の3バイトを再送せず、未処理の5バイトを続けます。実装では途中のエラーや進捗なしも扱い、すべて書けた場合にだけ同期と置換へ進みます。
7容量不足では公開中の名前を変えない
一時ファイルへの書込み途中でENOSPCになった場合を考えます。公開中の設定を古い版として、未完成の候補は置換しません。
published = b"mode=old\n"; candidate = b"mode="完全な旧版と途中の候補; write_ok = False書込み失敗; published = candidate if write_ok else published成功時だけ置換; print(published, candidate)旧版が残ったことを確認公開中の内容はoldのままです。一時ファイルを使っても、失敗を無視してrenameすると不完全な内容が公開されます。空き容量だけでなくI/Oエラーやfsync失敗も確認し、失敗時の一時ファイルを扱う手順が必要です。
8実際のファイルの最終状態
最初に開いた参照を閉じ、名前から読める設定を確認して一時ディレクトリを片付けます。
old.close()以前の参照を閉じる; print(config.read_bytes())現在の設定を読む; lab.cleanup()実験用ファイルを片付けるファイル操作の結果はmode=newです。この読取り確認は正常終了した操作の結果を確かめるもので、電源断試験の代わりにはなりません。停止位置を考える際は、内容の書込み、名前の切替え、それぞれの同期完了を別々に追います。
まとめ
writeの成功量を確認し、言語バッファのflushとOSのfsyncを順に行います。
renameの原子的な名前の切替えと、停止後の永続性は別の性質です。
途中で失敗した候補を公開せず、旧版を残せる手順にします。
操作履歴と解説を振り返れます。
Python 3
from pathlib import Path; import tempfile, os; lab = tempfile.TemporaryDirectory(); folder = Path(lab.name); config = folder / "config.txt"; config.write_bytes(b"mode=old\n"); old = config.open("rb"); print(old.read())9 b'mode=old\n'
pending = folder / "new.tmp"; f = pending.open("wb"); f.write(b"mode=new\n"); f.flush(); os.fsync(f.fileno()); f.close(); print(config.read_bytes(), pending.read_bytes())9 b'mode=old\n' b'mode=new\n'
os.replace(pending, config); print(config.read_bytes(), pending.exists()); old.seek(0); print(old.read())b'mode=new\n' False 0 b'mode=old\n'
durable_data = {"old": b"mode=old\n", "new": b"mode=new\n"}; durable_names = {"config": "old"}; live_names = dict(durable_names); live_names["config"] = "new"; print(durable_data[live_names["config"]], durable_data[durable_names["config"]])b'mode=new\n' b'mode=old\n'
durable_names = dict(live_names); recovered = durable_data[durable_names["config"]]; print(recovered)b'mode=new\n'
payload = b"ABCDEFGH"; accepted = 3; written = payload[:accepted]; remaining = payload[accepted:]; print(written, remaining); written += remaining; print(written == payload)b'ABC' b'DEFGH' True
published = b"mode=old\n"; candidate = b"mode="; write_ok = False; published = candidate if write_ok else published; print(published, candidate)b'mode=old\n' b'mode='
old.close(); print(config.read_bytes()); lab.cleanup()b'mode=new\n'
演習完了