2014/03/31 追記
安易に書いてましたがこの方法は実用レベルに達していません。
理由はS3では結果整合性が保証されていないためです。
この点はちょっと古い記事ですがDevelopers.IOさんの記事が詳しいです。
http://dev.classmethod.jp/cloud/amazon-s3-eventually-consistent-and-consistent-read/
つまり、シーケンス番号のソースファイルを多数のアクセスで読み込みまくると、
たまーに更新前の番号が取れてしまうケースが発生してしまうという事です。
よってこの方法はゆるーく言えばアクセス数がめちゃめちゃ少ないとか、
たまーに違っちゃうくらい他の処理で検知してリトライすんぜー
って場合のみ使ってください。
他に何か方法はないか!?って方は別記事
(DynamoDBでSEQUENCE)[http://qiita.com/supertaihei02/items/a3cf7b374ea5699f32e3]
をご覧ください。
目的
S3上のあるファイルでDBのSEQUENCE的に一意のnumberを管理したいんだ!
会員情報とかS3で管理してランニングコストを下げたいんだー!
やってみた
排他ロックかけないといけないよねーという事で、以下な感じでやってみた
1.registerStreamWrapperする
2.読み書きモード(r+)でfopenする
3.排他モード(LOCK_EX)でflockする
4.現在の値をfreadしてインクリメントしてftruncateしてfwrite
5.lock解除してfclose
じぇじぇじぇ
Warning: The Amazon S3 stream wrapper does not allow simultaneous reading and writing. >Mode not supported: r+. Use one 'r', 'w', 'a', or 'x'. in phar:///var/www/html/hihin/htdocs/flmodule/core/aws/aws.phar/Aws/S3/StreamWrapper.php on line 701
なん・・・だと・・・?
リーディングアンドライティングハキョカサレテマセンノデスカー!
マニュアル
You can upload larger files by streaming data using fopen() and a "w", "x", or "a" stream >access mode. The Amazon S3 stream wrapper does not support simultaneous read and write >streams (e.g. "r+", "w+", etc). This is because the HTTP protocol does not allow >simultaneous reading and writing.
そもそもflockできるのか試してみると
Warning: flock(): Aws\S3\StreamWrapper::stream_lock is not implemented!
Oh my god...
というわけで・・・
実装開始
その昔Perl界隈でよく使われていたrename方式というものを応用します。
手順は以下の通り
1.対象ファイルに現在時間情報をつけてリネーム
1-1.失敗した場合(ロック中)wait&timeout処理
2.読み込む
3.書き込む
4.元の名前にリネーム
require(dirname(__FILE__).'/aws.phar'); // load aws.phar
use Aws\Common\Aws;
use Aws\Common\Enum\Region;
use Aws\S3\Exception\S3Exception;
$accesskey = 'Your AWS Access Key'; // TODO
$secret = 'Your AWS Access Secret'; // TODO
$bucket = 'Your Bucket Name'; // TODO
$key = '/path/to/filedir/filename.txt'; // TODO
$unlocktime = 10; // TODO(sec)
$waittime = 1; // TODO(sec)
$retry = 5; // TODO
$sdir = 's3://'.$bucket.$dir;
$skey = $sdir.$lockfile;
$aws = AWS::factory(array(
'key' => $accesskey,
'secret' => $secret,
'certificate_authority' => false,
'region' => Region::AP_NORTHEAST_1
));
$client = $aws->get('s3');
$client->registerStreamWrapper();
$skey = 's3://'.$bucket.$key;
$sdir = dirname($skey);
$lockfile = basename($key);
$current = '';
$fail = false;
// lock
for($i=0;$i<$retry;$i++) {
echo "lock try ".($i + 1)."<br/>";
try {
if (rename($skey, $current = $skey.time())) {
echo "Get lock<br/>";
$fail = true;
break;
} else {
echo "Rename error<br/>";
}
} catch(S3Exception $e) {
echo "Locked check start<br/>";
}
sleep($waittime);
}
if (!$fail && $handle = opendir($sdir)) {
// ロックファイルを検索
while (($file = readdir($handle)) !== false) {
// ロックファイル + 時間 のファイルが存在した場合
if (preg_match('/^'.$lockfile.'([0-9]+)$', $file)) {
// ロックファイルの時間を取得
$time = preg_replace('/^'.$lockfile.'/', '', $file);
// 期限切れならロック解除
if (time() - $time > $unlocktime) {
echo "Destruction lock<br/>";
rename($sdir.$file, $current = $skey.time());
} else {
die("Process busy<br/>");
}
break;
}
}
closedir($handle);
}
// read.
if ($fp = fopen($current, 'r')) {
$seq = intval(fgets($fp), 0);
echo "Now sequence:".$seq."<br/>";
fclose($fp);
// write.
if ($fp = fopen($current, 'w')) {
$next = $seq+1;
fputs($fp, $next);
echo "Next sequence:".$next."<br/>";
fclose($fp);
}
}
// unlock
try {
if(rename($current, $skey)) {
echo "Unlock<br/>";
} else {
echo "Unlock failed<br/>";
}
} catch(S3Exception $e) {
echo "Unlock failed<br/>";
}
これで排他ロックできましたとさ。
時間をファイル名に付加する部分はS3のmetadataを使ってスマートにできないかと調査してみたんだけど、
ファイルをアップロードし直す必要がある(BodyをNULLにしてのputはエラー)事がわかったのでやむなくこんな感じになりました。
githubにS3用の独自ラッパーを置いてます。
aws_s3wrapper.php
これを使えば以下の様に簡単にロックが取れるようにしました。
$s3 = S3::getInstance();
$lockfile = 's3://bucketname/path/to/filedir/filename.txt';
// lock
$lockedfile = $s3->lock($lockfile);
// unlock
$s3->unlock($lockfile, $lockedfile)
以上