投稿ログの一部が勝手に削除される不具合 #42
Labels
No Label
bug
discussion
documentation
duplicate
enhancement
feature
help wanted
invalid
Priority
High
Priority
Low
Priority
Medium
question
wontfix
No Milestone
No project
No Assignees
2 Participants
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: stat2/delightly-v2fork#42
Loading…
Reference in New Issue
No description provided.
Delete Branch "%!s(<nil>)"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
概要
投稿ログ
/{$bbs}/LOG.cgi
の一部が勝手に消えてしまう不具合を確認しました。{$bbs}は板名を表します。不具合の修正方法と、それに伴う古い投稿ログの扱いについての問題提起です。
現象
/{$bbs}/LOG.cgi
の行数が多い場合に、複数の投稿が短時間で行われると古い投稿ログの一部が消えてしまうことがある。例えば、管理画面での投稿最大ログ保存件数は無記入(上限なし)の場合でも、
LOG.cgi
が約1万行から約4千行に減少しました。原因の推察と修正案
/test/bbs-main.php
で投稿ログのファイルLOG.cgi
を更新する際に、file
で開きfile_put_contents
で更新しているのが問題ではないかと考えています。該当部分(ca415d001d/test/bbs-main.php (L1127-L1136)
)fopen
→flock
→fread
→更新
→fwrite
→flock
→fclose
のようにファイルロックを活用した処理にすると改善する可能性があります。私の開発環境ではこちらの方法で改善されましたが、実際に多人数がアクセスする本番環境ではどうなるか不明です。修正コード案
修正に伴う古い投稿ログの扱い
この不具合が修正された場合、未設定だと投稿ログが無限に貯まってしまうことになり、別の不具合が発生する恐れがあります。
例えば、未設定の場合は上限なしではなく1万件までに制限する等の必要がありそうです。
まとめ
不具合発生については再現性がありますが、原因については推察止まりです。
不具合発生の原因究明及び修正コード案で改善が可能かどうかの確認をお手伝いいただきたいです。
また、別の修正案やご質問等があればお知らせください。
実を言うと、これは LOG.cgi に限った話ではありません。
flock()
はアドバイザリーロックなので、アドバイザリーロックを取っている相手にしか効果がありません。file_get_contents()
やfile()
にはアドバイザリーロックを取る機能がないため、排他制御が行われず、書き込んでいる途中のファイルを読み込んでしまうことが起こり得ます。書き込み途中のデータを読み込み、加工して、もう一度書き込むことで、データを欠損させたり、壊してしまったりするわけです。
今回はおそらく…
file_put_contents()
で書き込む(ロックを取っているつもり)file()
で4,000件分だけ読み込む(ロックを取らないので10,000件書き込むまで待ってくれない)file_put_contents()
で4,001件書き込む…という感じでしょう。
重要なことなのでもう一度書きますが、
file_get_contents()
やfile()
にはアドバイザリーロックを取る機能がないため、排他制御が行われません。この少し上の箇所では
$subjectfile
をfile_get_contents()
で読み込んでfile_put_contents(..., LOCK_EX)
で書き込んでいます。ここも実は排他制御が行われていません。
書き込みが同時に起きなかったり、ファイルサイズが小さいため問題にならないだけで、ファイルサイズが大きくなると壊れます。
delightly では同じ問題を抱えている箇所がたくさんあります。近いうちに
file_get_contents()
撲滅運動をすることになるでしょう。さて、LOG.cgi に限ると、解決する方法は複数あります。
まず、
file()
をfopen() + flock() + fread()
の読み込みに置き換えるパターン。この場合、上記のように数千件のデータが消失することはなくなりますが、同時に書き込まれた場合は最悪1件のデータがなかったことになります。
次に konkon-fox さんの提示しているコードのようなパターン。
全体が
flock()
されているため、データがなかったことにされる可能性はありませんが、ずっと排他ロックしているためパフォーマンスが最悪になります。実況にはあまり向かないように思います。
個人的にオススメしたいのは「ログの並び順を逆にする」パターンです。
現在の LOG.cgi は新しい順にソートされており、そのため毎回ファイル全体を書き込む必要があります。
これを古い順にする(つまり普通のdatと同じ順にする)と追記モードで書き込めばいいだけになります。
あとは test/operate/log.php でログを表示する前に
array_reverse()
するだけです。ただし、並び順を逆にしたとしても縮小処理のときは排他ロックをする必要があります。なので…
flock(LOCK_SH)
で共有ロックを取って LOG.cgi に追記モードで書き込むflock(LOCK_EX)
で排他ロックを取って縮小処理をするという感じになると思われます。
ファイルのロックに関してあまり詳しくなかったので、なんとなくこんな状態なのだろうという認識を詳しく解説していただいて非常に助かります。
$subjectfile
等の他の箇所でも排他制御が行われておらず問題なのではないかと危惧しておりましたがその通りのようですね。同様の問題ということなので
LOG.cgi
だけでなくファイルの更新処理はまとめて修正するのが良いのでしょうか。LOG.cgi
の修正方法に関してはご提案の「ログの並び順を古い順にして追記する方式」が理にかなっていると思います。とありますが、共有ロック状態では書き込みが行えないので排他ロックを行う必要があると思うのですがどうなのでしょうか。
細かい話をするといろいろあるんですが、排他制御が完全に壊れている現状よりはマシになるだろうと思います。
書き込みを行えないわけではありません。
追記モードであれば書き込みはアトミック動作なので共有ロックを取る必要もありません。
ここで共有ロックを取っているのは縮小処理中に書き込みを行わないため、または書き込みが終わるまで縮小処理の実行を待つためです。
もちろん排他ロックでもいいのですが、 ここは性能上のボトルネックになるので共有ロックで済むならそうしたほうがいいかなと思いまして。
今回はとりあえず
LOG.cgi
更新部分の修正のみを考えることにします。alice2chさんのご助言を取り入れてコード案を再修正しました。よろしければ目を通してもらえるとありがたいです。
無限にログが増えることを防ぐために、ログ保存件数の最大値
$LOG_LIMIT
を定義しています。お待たせしました。
追記モードのまま
ftruncate()
するのはなんというか居心地が悪いですね・・・。試したところ動いていましたが
'c+'
で開き直してもいい気がします。fgets()
をfor文で回してカウントするのは省メモリでいいですね。逆にここは
$logData
と$logDataLines
と$newLogData
で元ファイルの約3倍の文字列が生成されています。シンプルに
fgets()
とfputs()
を使う方が省メモリにできそう。あとは
$SETTING['LOG_LIMIT']
の最大値が本当に1万件でいいのか、という問題があります。1万件は10スレ分でしかないので、余裕を見て10万件くらい欲しい場合も結構あるような・・・?
でも、多すぎても万が一の流出時が怖いかもしれませんからね。うーん・・・。
コードを再修正してみました。
ログの行数を確認して超過しているようなら
c+
でファイルを開き直して縮小処理を行っています。縮小処理後の
fwrite
による書き込みについては、配列に格納されたログをforeach
で行ごとに書き込む方法とimplode
で結合してから書き込む方法を考えましたが、メモリ使用量はあまり変わらず処理時間は後者が圧倒的に短かったので後者を採用しました。$LOG_LIMIT = 10000
については仮の値で10000を入れましたが、正直どれくらいの値が適切なのかよく分かりません。上限は間違いなく設定すべきだとは思うのですが…