プログラムの事とか

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

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プラン時だけ?)何も考えずにロックしまくると大変だよ

結論

排他制御はやればできる

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