S3上でSEQUENCE（PHP）

## 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

なん・・・だと・・・？
リーディングアンドライティングハキョカサレテマセンノデスカー！
[マニュアル](http://docs.aws.amazon.com/aws-sdk-php/guide/latest/feature-s3-stream-wrapper.html)
>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.元の名前にリネーム

```php

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](https://github.com/supertaihei02/aws_s3wrapper.php)
これを使えば以下の様に簡単にロックが取れるようにしました。

```php

$s3 = S3::getInstance();
$lockfile = 's3://bucketname/path/to/filedir/filename.txt';
// lock
$lockedfile = $s3->lock($lockfile);
// unlock
$s3->unlock($lockfile, $lockedfile)

```

以上