より多くの環境で動作させるために

CGIスクリプトを組む時などに問題になる事の一つにファイルのロックがあります。
排他制御をするための代表的なものとして、以下の三つの手法が挙げられます。
  • flock()
  • ディレクトリを利用するもの
  • シンボリックリンク(ショートカットファイル)を利用するもの
通常はflock()を使えばいいのですが、環境によっては利用できません。
シンボリックリンクについても利用できない環境があるため、より多くの環境で使えるようにするためにはディレクトリを利用します。

ディレクトリを使ったロックの問題点

Perlでディレクトリを使ったファイルロックを実現する簡単なコードは以下のようになります。

#ロック関数
sub Lock
{
  #20回試行する
  for(1 .. 20)
  {
    if(mkdir('lock', 0755))
    {
      #ロック成功
      return 1;
    }
    
    sleep(1);
  }
  
  #ロック失敗
  return 0;
}

#アンロック関数
sub UnLock
{
  rmdir('lock');
}

ディレクトリを利用した場合に問題になるのが、「スクリプトが異常終了した場合にディレクトリが残ってしまう」事です。
そこで多くのスクリプトでは、「作成されてから○分以上経ったディレクトリは異常終了で残ってしまったものと見なし削除する」という処理を加えています。

#ロック関数
sub Lock
{
  #ディレクトリが作成されて5分以上経っていれば削除
  if(-d 'lock' && (stat('lock'))[9] < time() - 300)
  {
    rmdir('lock');
  }
  
  #以下同様

しかしこの処理を加える事で新たな問題が発生します。
ディレクトリが古いと判断するタイミングが複数のプロセスで重なってしまうと、片方のプロセスが削除・作成した後にもう片方のプロセスが再度削除・作成を行う可能性が出てきてしまいます。



0th server で利用しているモジュール

0th server で利用しているファイルロックでは、ディレクトリ名に時間を用いる事で問題を上手く回避しています。
このモジュールを使用すると、「lock」ディレクトリの中にキー文字列の名前のディレクトリ、その中にUnix time文字列の名前のディレクトリが作成されます。
このモジュールの欠点を挙げるとすれば、ファイルをロックするためだけのものとしてはボリュームが大きいという事でしょう。

Source(Perl)


#ロックに使うディレクトリ
my $LOCK_DIR = 'lock/';

#ロックしたkeyと時間を保存するハッシュ
my %LOCK_TIME;

#ロックのトライ回数
my $TRY_TIME = 20;

#ロックを異常終了で残ったと判断する時間(秒)
my $ERR_TIME = 300;

#############################################################################
# 初期処理
#
mkdir($LOCK_DIR, 0755);

#############################################################################
# ディレクトリのリストを取得(ローカル関数)
#   return
#     取得したリスト
#   parameter
#     1:Path  in  取得するパス
#
my $GetDirList = sub( $; )
{
  my ($Path) = @_;
  
  my @DirList;
  
  opendir(DIR, $Path);
  for(readdir(DIR))
  {
    if($_ ne '.' && $_ ne '..')
    {
      push(@DirList, $_);
    }
  }
  closedir(DIR);
  
  return @DirList;
};

#############################################################################
# ロック
#   return
#     成功:非0/失敗:0
#   parameter
#     1:BaseName  in  ロックに使うkey文字列
#
sub Lock_Lock( $; )
{
  my ($BaseName) = @_;
  
  my $LockPath = $LOCK_DIR . $BaseName . '/';
  my $Try = $TRY_TIME;
  my $Time = time;
  
  #BaseNameの正当性チェック
  if($BaseName =~ m{[/\\:*?"<>|]})
  {
    return 0;
  }
  
  #ロックに使うディレクトリ作成
  mkdir($LockPath, 0755);
  
  #順番の確保
  while(!mkdir($LockPath . $Time, 0755))
  {
    $Try--;
    if($Try == 0)
    {
      return 0;
    }
    
    select(undef, undef, undef, 0.2);
    $Time = time;
    mkdir($LockPath, 0755);
  }
  
  #異常終了で残ったロックの削除
  for(sort(&$GetDirList($LockPath)))
  {
    if($_ < $Time - $ERR_TIME)
    {
      rmdir($LockPath . $_);
    }
    else
    {
      last;
    }
  }
  
  #順番待ち
  while((sort(&$GetDirList($LockPath)))[0] != $Time)
  {
    $Try--;
    if($Try == 0)
    {
      rmdir($LockPath . $Time);
      return 0;
    }
    
    select(undef, undef, undef, 0.2);
  }
  
  $LOCK_TIME{$BaseName} = $Time;
  
  return 1;
}

#############################################################################
# アンロック
#   return
#     成功:非0/失敗:0
#   parameter
#     1:BaseName  in  ロックに使ったkey文字列
#
sub Lock_UnLock( $; )
{
  my ($BaseName) = @_;
  
  if(!$LOCK_TIME{$BaseName})
  {
    return 0;
  }
  
  if(!rmdir($LOCK_DIR . $BaseName . '/' . $LOCK_TIME{$BaseName}))
  {
    if(! -d $LOCK_DIR . $BaseName . '/' . $LOCK_TIME{$BaseName})
    {
      delete($LOCK_TIME{$BaseName});
    }
    
    return 0;
  }
  
  delete($LOCK_TIME{$BaseName});
  rmdir($LOCK_DIR . $BaseName);
  
  return 1;
}

#############################################################################
# ロックのトライ回数の取得
#   return
#     トライ回数
#
sub Lock_GetTryTime()
{
  return $TRY_TIME;
}

#############################################################################
# ロックのトライ回数の設定
#   parameter
#     1:TryTime in  トライ回数
#
sub Lock_SetTryTime( $; )
{
  my ($TryTime) = @_;
  $TRY_TIME = $TryTime;
  return;
}

#############################################################################
# ロックを異常終了で残ったと判断する時間の取得
#   return
#     ロックを異常終了で残ったと判断する時間(秒)
#
sub Lock_GetErrorTime()
{
  return $ERR_TIME;
}

#############################################################################
# ロックを異常終了で残ったと判断する時間の設定
#   parameter
#     1:ErrorTime in  ロックを異常終了で残ったと判断する時間(秒)
#
sub Lock_SetErrorTime( $; )
{
  my ($ErrorTime) = @_;
  $ERR_TIME = $ErrorTime;
  return;
}

1;

ロックをしてもファイルは壊れる

ファイルロックを行っていても、ファイルに書き込みを行っている最中に異常終了してしまうとファイル内容は壊れてしまいます。
そこで 0th server では以下のような関数でファイルが壊れる確率を下げています。

#############################################################################
# ファイル書き出し
#   return
#     成功:非0/失敗:0
#   parameter
#     1:FileName  in  ファイル名
#     2:TempName  in  一時ファイル名
#     3:Source    in  書き出す内容の配列
#
sub FileIO_Write( $$@; )
{
  my ($FileName, $TempName, @Source) = @_;
  
  if(open(my $Fh, '>' . $TempName))
  {
    print $Fh @Source;
    close($Fh);
    
    return rename($TempName, $FileName);
  }
  else
  {
    return 0;
  }
}

コメント

コメントはまだありません。

name: message:
全部読む 最新50件 長文書き込み
説明は以上だ。質問はあるか?