【ITニュース解説】Rewriting a JSON File in Place Let My Reader See It Half-Written: Temp File, fsync, os.replace and Three Traps
2026年10月23日に「Dev.to」が公開したITニュース「Rewriting a JSON File in Place Let My Reader See It Half-Written: Temp File, fsync, os.replace and Three Traps」について初心者にもわかりやすく解説しています。
ITニュース概要
ファイルを直接上書きすると、読み取り中に破損したり、クラッシュで壊れたりする危険がある。これを避けるには、新しい内容を一時ファイルに完全に書き込み、ディスクに同期後、元のファイルとアトミックに置き換える手法が安全だ。一時ファイルの場所やパーミッション、シンボリックリンクに注意が必要。
ITニュース解説
ITシステムでは、複数のプログラムが同時にファイルを読み書きすることは珍しくない。しかし、この一見単純な操作にも落とし穴があり、データ破損や不完全なファイル読み込みといった問題が発生することがある。特にJSONファイルのように構造が厳密なデータを扱う場合、中途半端な状態でファイルが読み込まれると、「JSONDecodeError: Expecting value: line 1 column 1 (char 0)」のようなエラーが発生し、プログラムが停止してしまう。
この問題の典型的な原因は、ファイルを「その場で書き換える」方式にある。たとえば、Pythonでopen(path, 'w')を使ってファイルを開くと、新しいデータを書き込む前にファイルの内容がすべて空にされてしまう。もし、このファイルが空になった瞬間や、まだ書き込み途中である瞬間に別のプログラムがファイルを読み込もうとすると、空のファイルや部分的にしか書き込まれていない不完全なファイルを読み取ってしまう。さらに悪いことに、書き込み処理の途中でプログラムがクラッシュした場合、元のファイルの内容は消え去り、中途半端なデータがディスクに残ってしまうため、リトライしても解決しない深刻なデータ破損につながる可能性がある。これは、読者が一時的に壊れたファイルを読み取る「一時的な破損」よりも、復旧が困難な「永続的な破損」である。
このような問題を回避し、ファイルを安全に更新するための確実な方法は、「アトミックなファイル更新」と呼ばれる手法を用いることである。アトミックとは、処理が完全に成功するか、あるいは全く変更が行われなかったかのどちらかであり、途中の不完全な状態が外部から見えないことを意味する。この手法では、まず元のファイルとは別の「一時ファイル」に新しいデータを完全に書き込む。そして、新しいデータがディスクに完全に書き込まれたことを確認した後、その一時ファイルを一瞬で元のファイルの名前に置き換える。Pythonのos.replace関数は、この置き換え処理をアトミックに行う機能を提供しており、システムがファイルを入れ替えるほんの一瞬の間、読者は古いファイルか新しいファイルのどちらか一方のみを参照し、決して中途半端な状態のファイルを見ることはない。
このアトミックな更新を実現する上で、いくつか注意すべき「落とし穴」が存在する。
一つ目の落とし穴は、一時ファイルを作成する場所に関わる。tempfile.mkstemp()のような関数を呼び出す際に、一時ファイルの保存場所を指定しないと、システムがデフォルトで指定する一時ディレクトリ(Linuxなら/tmpなど)に作成されることがある。しかし、この一時ディレクトリが、元のファイルが存在するディレクトリとは異なるファイルシステム(異なるディスクやパーティション)上にある場合、os.replaceは失敗してしまう。これは、os.replaceがファイルシステムをまたいでファイルを移動できないためで、「Invalid cross-device link (無効なクロスデバイスリンク)」といったエラーが発生する。この問題の厄介な点は、開発環境では同じファイルシステム上にあるため問題なく動作しても、本番環境では異なるファイルシステム上でエラーになる可能性があることだ。これを避けるためには、一時ファイルは必ず元のファイルと同じディレクトリ内に作成する必要がある。
二つ目の落とし穴は、ファイルパーミッションの問題である。tempfile.mkstemp()で作成される一時ファイルは、デフォルトでは所有者のみが読み書きできるような、セキュリティ上最も厳しいパーミッション設定になっていることが多い。この一時ファイルをos.replaceで元のファイルと置き換えると、元のファイルが持っていた「他のユーザーやプログラムにも読み書きを許可する」パーミッションが失われてしまい、置き換え後のファイルは所有者以外からは読み取れなくなる可能性がある。これにより、ウェブサーバーや他のサービスがファイルの読み込みに失敗し、「PermissionError (パーミッションエラー)」が発生することがある。この問題の解決策は、os.replaceを実行する前に、os.chmodを使って元のファイルからパーミッション情報を取得し、それを一時ファイルに設定し直すことである。
三つ目の落とし穴は、シンボリックリンクを扱う場合である。もし、更新対象のファイルが実際にはシンボリックリンク(別のファイルを指し示すショートカットのようなもの)だった場合、os.replaceを単純に使うと、シンボリックリンクが指す先のファイルを更新するのではなく、シンボリックリンクそのものを新しいファイルで置き換えてしまう可能性がある。結果として、シンボリックリンクが指していた本来のファイルは更新されず古い情報のままであり、他のプログラムがその古いデータを読み続けてしまうことになる。これを防ぐためには、os.path.realpath関数を使って、シンボリックリンクが最終的に指し示す「本当のパス」を特定する必要がある。そして、一時ファイルをその本当のパスが示すディレクトリに作成し、その実体を更新することで、シンボリックリンク自体はそのまま残しつつ、指し示す先のファイルを安全に更新できる。
これらの落とし穴を回避した堅牢な実装では、データを一時ファイルに書き込んだ後、f.flush()でバッファ内のデータをOSに渡し、os.fsync(f.fileno())を呼び出すことで、OSのバッファから物理的なディスクデバイスへ確実にデータが書き込まれたことを保証する。さらに、ファイル名を変更するという操作は、実際にはファイルそのものの内容ではなく、ファイルが格納されている「ディレクトリ」の内容を変更するものであるため、os.replaceの後にそのディレクトリに対してもos.fsync()を実行することが推奨される。これにより、ファイル名の変更という情報もディスクに永続化される。もし書き込み途中でエラーが発生した場合は、残った一時ファイルを確実に削除する処理も重要である。
このアトミックなファイル更新手法を用いることで、ファイルの読み書きが同時並行で行われるような環境でも、読者が不完全なファイルや破損したファイルを読み取るリスクを大幅に低減できる。これにより、プログラムの安定性が向上し、データの信頼性が保証される。ただし、この手法は、複数のプログラムが同時にファイルを「書き込む」競合状態(ライトコンフリクト)を解決するものではなく、その場合はさらにロック機構などの並行処理制御が必要になる点には注意が必要だ。しかし、ファイルを安全に更新するための基本的なアプローチとして、この一時ファイルを用いたアトミックな置き換えは、システムエンジニアが堅牢なアプリケーションを構築する上で不可欠な技術である。