Perlによるファイルの排他制御を考える flock編

Perlでファイルの排他制御を行うにあたり、実運用に耐えるコーディングについて考えます。
今回はflockを用いた排他制御です。

flockは完璧ではない

flockによるファイルの排他制御は「ファイルが壊れないようにする」処理ではありません。
「ファイルを壊れにくくする」処理です。
本気で壊れたらまずいファイルについては、排他制御を必要としない運用方式や、データベース等の導入を検討すべきです。

マルチスプロセス環境でのflockについて

PHPではマルチプロセス環境でのflockに信頼性がありませんが、Perlの場合は気にしなくても良いようです。
ただ、flockがスレッドレベルロック+プロセスレベルロックなのかどうは分かりません。

懸念する点

open関数

open関数は正しく使わないと問題が生じます。
以下の一文は、openを行った際にファイルサイズをゼロ(ファイルを空っぽ)にしてしまいます。

open(out,"> test.txt");

読み書きモードとして、openした時点でファイル内容は維持するためには、以下の一文を使います。

open(out, "+< D:/test.txt");

なお、追記も完全ではなく、同時アクセスが生じた場合はデータが混ざる場合があります。

flock関数

flock関数は、その挙動を理解して使わないと運用に異常を来たします。
flockでは、そのオペレーションに以下の方法を指定する事が可能です。

1 読み込みロック+ブロック
2 書き込みロック+ブロック
5 読み込みロック
6 書き込みロック
8 ロックを解除

1(読み込みロック+ブロック)と2(書き込みロック+ブロック)は、flockしたファイルをブロックします。
つまり、別のプロセスがブロックされたファイルにアクセスすると、ロックを解除するまで待たされる事になるのです。

alarm関数

alarm関数は、排他制御の処理においては、タイムアウトを行うために使用する場合が多いと思います。
しかし、flock関数でプロセスを「ブロック」してしまうと、せっかくのタイムアウト処理が有効になりません。
つまり、プロセスの数と処理時間に比例して、待ち時間が無尽蔵に増加してしまうのです。

close関数

close関数は、書き込みキャッシュをフラッシュすると同時に、flockによるファイルロックを解除します。
close関数の前にflockにてロックを解除してはいけません。
ロックを解除してしまうと、キャッシュがフラッシュされていない、不完全なファイルが公開されてしまいます。
・・・と書きましたが、最近のPerlではflockでファイルロックを解除すると、キャッシュがフラッシュされます。
確実に排他制御する観点から、説明させていただきました。

検討すべき点

ファイルへの書き込みと読み込み

ファイルロックを非ブロックでかけます。
flock関数において、5(読み込みロック)と6(書き込みロック)は、自分がロックをかける事ができた場合は「真」、それ以外は「偽」を返すので、これを利用してループさせます。
ループの際には、サーバに負担をかけないために、sleepさせるのが良いかも知れません。

タイムアウト

alarm関数とeval関数を組み合わせて行います。
プロセスがブロックされていない事が前提ですが、アラームシグナルを受け取りdieする等するのが簡単です。
alarm関数によるアラームの初期化は、evalの内側と外側に記述すると、より安全だと思います。

サンプル

上記の事を加味し、実際にコーディングしてみました。

ファイル書き込み
#!D:/usr/local/Perl/bin/Perl.exe

print "◆ファイルを排他制御します。\n";

eval{
	# Alarmシグナルキャッチ時
	local $SIG{ALRM} = sub { die "timeout"; };

	# 前のプロセスの終了〜ファイル書き込みに、10秒間だけ待つ
	#  一連の処理が10秒間以内に終わらないとタイムアウトします。
	alarm(10);

	# ファイルを読み書きモードで開く
	#  test.txtがないとエラーになります。
	if(! open(out, "+< test.txt")){
		die "fileopen";
	}
	print "ファイル開きました(10秒間でタイムアウト)。\n";

	# ファイルロック
	#  ロックが解除されるまでループ
	while(! flock(out,6)){
		sleep(1);
	}
	print "ファイルをロックしました。\n";

	# もしテストするとしたら、ここで無限ループ
	#  以下は無限ループのサンプルです。
	#while(){;}

	# ファイルサイズをゼロにする
	if(! truncate(out,0)){
		die "filetruncate";
	}

	# ファイルの先頭を指定
	if(! seek(out,0,0)){
		die "fileseek";
	}

	# ファイル書き込み
	if(! print out "ファイルへ書き込みます。\n"){
		die "fileprint";
	}
	print "ファイルに書き込んでいます。\n";

	# ファイルをクローズ
	#  ファイルをクローズすると自動的にロックも解除されます。
	close(out);
	print "ファイルをクローズしました。\n";

	# Alarmを初期化
	alarm(0);
};

# Alarmを初期化(dieした時のため)
alarm(0);

# ファイルをクローズ(dieした時のため)
close(out);

if($@ =~ /timeout/){
	print "ファイル書き込みがタイムアウトしました。\n";
}elsif($@ =~ /fileopen/){
	print "ファイルを開けませんでした。\n";
}elsif($@ =~ /fileseek/){
	print "ファイルの先頭を指定できませんでした。\n";
}elsif($@ =~ /fileprint/){
	print "ファイルの書き込みに失敗しました。\n";
}elsif($@ =~ /filetruncate/){
	print "ファイルサイズの変更に失敗しました。\n";
}elsif($@){
	print "その他のエラーです。[".$@."]\n";
}

if($@){
	exit(1);
}

print "おしまい\n";

exit(0);
ファイル読み込み
#!D:/usr/local/Perl/bin/Perl.exe

print "◆ファイルを排他制御して読み込みます。\n";

eval{
	# Alarmシグナルキャッチ時
	local $SIG{ALRM} = sub { die "timeout"; };

	# 前のプロセスの終了〜ファイル書き込みに、10秒間だけ待つ
	#  一連の処理が10秒間以内に終わらないとタイムアウトします。
	alarm(10);

	# ファイルを読み書きモードで開く
	#  test.txtがないとエラーになります。
	if(! open(in, "test.txt")){
		die "fileopen";
	}
	print "ファイル開きました(10秒間でタイムアウト)。\n";

	# ファイルロック
	#  ロックが解除されるまでループ
	while(! flock(in,5)){
		sleep(1);
	}
	print "ファイルをロックしました。\n";

	# もしテストするとしたら、ここで無限ループ
	#  以下は無限ループのサンプルです。
	#while(){;}

	# ファイルを読み込む
	@in = <in>;

	# ファイルをクローズ
	#  ファイルをクローズすると自動的にロックも解除されます。
	close(in);
	print "ファイルをクローズしました。\n";

	# Alarmを初期化
	alarm(0);
};

# Alarmを初期化(dieした時のため)
alarm(0);

# ファイルをクローズ(dieした時のため)
close(in);

if($@ =~ /timeout/){
	print "ファイル書き込みがタイムアウトしました。\n";
}elsif($@ =~ /fileopen/){
	print "ファイルを開けませんでした。\n";
}elsif($@){
	print "その他のエラーです。[".$@."]\n";
}

if($@){
	exit(1);
}

# ファイル内容を表示
print "ファイルの内容 : ";
print @in;

print "おしまい\n";

exit(0);