プログラムの事とか

お約束ですが「掲載内容は私個人の見解です」

Azure Functionsに排他制御を入れる

FunctionsでFunction単位で排他制御したいことがあったので調べてみました

結論はBLOB作ってそれをロックでした(他にあるのかしらない)

BLOBのロックは

Microsoft Azure Storage でのコンカレンシー制御の管理 | Microsoft Docs

ここに書いてあります

ということで実験しましょう

HttpTriggerなFunctionを作る

こんな感じ

public static class Function1
{
    [FunctionName("Function1")]
    public static async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
        [Table("Log")] CloudTable logTable,
        ILogger log)
    {
        string name = req.Query["name"];

        for (var i = 0; i < 5; i++)
        {
            await logTable.ExecuteAsync(TableOperation.Insert(new LogEntity(name, i)));
            await Task.Delay(1000);
        }

        return new OkResult();
    }

    public class LogEntity : TableEntity
    {
        public string Name { get; set; }
        public int Num { get; set; }
        public LogEntity()
        {
            PartitionKey = "Log";
            RowKey = Guid.NewGuid().ToString();
        }
        public LogEntity(string name, int num) : this()
        {
            Name = name;
            Num = num;
        }
    }
}

テンプレートをいじってテーブルストレージ(Log)にデータを保存します

同時制御したいので書き込んで1秒待って、を5回繰り返します

あとはデバッグ実行して

http://localhost:7071/api/Function1?name=a
http://localhost:7071/api/Function1?name=b

で呼び出し

書き込んだ値を時間でソートすると

f:id:puni-o:20181130184226j:plain

二つのリクエストが同時に走っていることがわかると思います

排他制御を入れて順番に実行する

Run関数にBLOBのロック処理を追加します

public static async Task<IActionResult> Run(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
    [Table("Log")] CloudTable logTable,
    Binder binder,
    ILogger log)
{
    string name = req.Query["name"];

    // BLOBをロックすることで排他制御とする
   #region リース取得
    var lockBlob = await binder.BindAsync<CloudBlockBlob>(new BlobAttribute("temp/lock"));
    if (!await lockBlob.ExistsAsync()) await lockBlob.UploadTextAsync("lock");  // 無ければ作っているけど事前に作っておいた方がいいと思う
    var lease = "";
    for (var i = 0; i < 100; i++)   // 20秒挑戦してダメなら終わろう
    {
        try
        {
            lease = await lockBlob.AcquireLeaseAsync(TimeSpan.FromSeconds(15), null);
            Console.WriteLine($"blob lease = {lease}");
            break;
        }
        catch (StorageException storageException)
        {
            if (storageException.RequestInformation.HttpStatusCode == (int)HttpStatusCode.Conflict)
            {
                Console.WriteLine("Wait for release lease...");
                await Task.Delay(200);
                continue;
            }
            return new ObjectResult("Error") { StatusCode = (int)HttpStatusCode.Conflict };
        }
    }

    if (string.IsNullOrEmpty(lease))
    {
        // 取れなかったらエラーで終わる
        log.LogWarning("Can't get lease.");
        return new ObjectResult("Error") { StatusCode = (int)HttpStatusCode.Conflict };
    }
    var accessCondition = AccessCondition.GenerateLeaseCondition(lease);
   #endregion

    for (var i = 0; i < 5; i++)
    {
        await logTable.ExecuteAsync(TableOperation.Insert(new LogEntity(name, i)));
        await Task.Delay(1000);
    }

    // リース解放
    await lockBlob.ReleaseLeaseAsync(accessCondition);

    return new OkResult();
}

実行結果

f:id:puni-o:20181130184616j:plain

順番に実行されるようになりました

注意点とか

コピペすると行数が膨らんで楽しい

BLOBのリース取得部分をクラスにしてもっと簡単に使えるようにしていた人がいました。探せば出てくると思うよ

リース期間に注意

AcquireLeaseAsyncに渡せるのは確か15~60秒だったはずなので注意

リース取得のリトライに注意

Functionsはデフォルトで5分長くても10分動き続けると強制的に落とされるので(Consumptionプラン時だけ?)何も考えずにロックしまくると大変だよ

結論

排他制御はやればできる

けどやらないに越したことはない

フォルダ名の最後がスペースのフォルダを作る (作れない)

作れません

知っている人は当たり前だと思うかもしれませんが、そんな名前のフォルダ作ろうなんて思ったこともなかったので知らなかったよ

試してみる

Explorer

f:id:puni-o:20181108183446j:plain

こうすると

f:id:puni-o:20181108183458j:plain

こうなります。勝手に最後のスペースが消されます。警告とかそーいうの出ません

勝手に省略しないで

f:id:puni-o:20181108183517j:plain

こっちの仲間に入れてあげればいいのに

C#でやってみる

// 最後がスペースのパス
var path = @"g:\test ";
try
{
    Directory.CreateDirectory(path);  // これは正常終了する
}
catch
{
    Console.WriteLine("しっぱい");  // 例外とか吐かない
}

if (Directory.Exists(path))  // これもtrueが返ってくる
{
    var file = Path.Combine(path, "a.txt");  // あるはずフォルダの下にテキストを書いてみる
    try
    {
        File.WriteAllText(file, "にゃーん");
    }
    catch (Exception e)
    {
        Console.WriteLine(e.Message);  // 「パス 'g:\test \a.txt' の一部が見つかりませんでした。」
    }
}

Oh...

せめてDirectory.Exists()falseを返してほしいんだけど・・なんか歴史的な経緯とかあるのかな~

GitHubから脆弱性あるよって報告が来た話

3か月ほど前に

ASP.NET CoreのSignalRでバイナリーデータをやり取りする - プログラムの事とか

ってやつを書いた時に作ったサンプル用のリポジトリGitHubからAlertがやってきました。 こんなの初めてだったからドキドキしちゃった💛

GitHubのInsights->Alertsを見るとこんなものが

f:id:puni-o:20181017091731j:plain

CVE-2018-8409はこちら

NVD - CVE-2018-8409

System.IO.Piplinesの4.5.0に脆弱性があったみたいですね

NuGetで最新にしてコミットして完了

通知見た時は誰か暇な人がIssueでも投げてくれたのかと思ったんだけど、GitHubが自動で解析みたいなことして警告出してくれているんですねー