Blog

[perl] Error in tempfile() using template /tmp/XXXXXXXXXX: Have exceeded the maximum number of attempts (1000) to open temp file/dir at hoge.pl line 14. のようなエラーが発生した場合の対処方法

File::Temp を使っていると、表題のようなエラーが発生することがある。 これは一件すると、ファイルを開く処理が失敗した用に見えて、ファイル開き過ぎとかかなあと思うんだけど、実際はそういうことではないようだ。

File::Temp の実装はこのへんになっていて、要するに EEXIST が発生した場合に 1000 回ファイル名を変えてリトライして、その結果としてダメだったというときにいい感じにあがる例外である。 https://github.com/Perl-Toolchain-Gang/File-Temp/blob/master/lib/File/Temp.pm#L602

ここを見れば分かる通り、EEXIST の時だけしか次のループにいかないので、OS 的な問題ではない。 https://github.com/Perl-Toolchain-Gang/File-Temp/blob/master/lib/File/Temp.pm#L529

ファイル名のテンプレートは /tmp/XXXXXXXXXX となっており、デフォルトでは X が 10 文字である。10 文字に対して、X は [0-9a-zA-Z_] のうち一文字が使用されるので、63パターンもある。

my @CHARS = (qw/ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
                a b c d e f g h i j k l m n o p q r s t u v w x y z
                0 1 2 3 4 5 6 7 8 9 _
            /);

ということは、984930291881790849 通りのパターンがあるので、通常ならばコンフリクトすることはない。

しかし、このファイル名の生成は Perl5 の rand() 関数を用いている。Perl5 の rand() 関数は fork() した場合に seed の再生成が行われることはないので、 conflict する可能性が考えられる。

以下のような再現コードにより、まさにその現象が起きていることが確認できる。

use strict;
use 5.018000;

use File::Temp qw/tempfile/;

rand(); # set seed

my $pid = fork() // die $!;
if ($pid) {
    # parent
    sleep 1;
    eval {
        tempfile();
    };
    say $@;
    kill $pid;
    waitpid($pid, -1);
} else {
    my @tempfiles;
    for (1..2000) {
        push @tempfiles, tempfile();
    }
    warn "created";
    sleep 2;
    exit 1;
}

以上まで調査した結果、この事象はマニュアルに書いてあることが判明した。

If you are forking many processes in parallel that are all creating
temporary files, you may need to reset the random number seed using
srand(EXPR) in each child else all the children will attempt to walk
through the same set of random file names and may well cause themselves to
give up if they exceed the number of retry attempts.

以上です。よろしくお願いします。