Azure Blob に対して、同じBlobを複数のワーカーから同時更新する

タイトル名のようなことが簡単に出来るのでは？と思いトライしてみたが、残念ながら、次のようなエラーをもらった。プログラム的には、`Parallel.ForEach` を使って、32 ワーカー（仮）から、同じBlobを同時更新するようなコードを書いた。

自分のイメージでは、その32 のワーカーは同時刻には同じ内容を返すはずなので、32のワーカーのどれが勝っても問題なく、楽観ロックすら必要ないので、楽勝と思っていたが違った。

```
Microsoft.Azure.Storage.StorageException: 'The MD5 value specified in the request did not match with the MD5 value 

This exception was originally thrown at this call stack:
    Microsoft.Azure.Storage.Core.Executor.Executor.ExecuteAsync<T>(Microsoft.Azure.Storage.Core.Executor.RESTCommand<T>, Microsoft.Azure.Storage.RetryPolicies.IRetryPolicy, Microsoft.Azure.Storage.OperationContext, System.Threading.CancellationToken) in Executor.cs
    System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(System.Threading.Tasks.Task) in TaskAwaiter.cs
    System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(System.Threading.Tasks.Task) in TaskAwaiter.cs
    System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd(System.Threading.Tasks.Task) in TaskAwaiter.cs
    Microsoft.Azure.Storage.Blob.CloudBlockBlob.UploadFromStreamAsyncHelper(System.IO.Stream, long?, Microsoft.Azure.Storage.AccessCondition, Microsoft.Azure.Storage.Blob.BlobRequestOptions, Microsoft.Azure.Storage.OperationContext, Microsoft.Azure.Storage.Core.Util.AggregatingProgressIncrementer, System.Threading.CancellationToken) in CloudBlockBlob.cs
    System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(System.Threading.Tasks.Task) in TaskAwaiter.cs
    System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(System.Threading.Tasks.Task) in TaskAwaiter.cs
    Microsoft.Azure.Storage.Blob.CloudBlockBlob.UploadFromByteArrayAsync(byte[], int, int, Microsoft.Azure.Storage.AccessCondition, Microsoft.Azure.Storage.Blob.BlobRequestOptions, Microsoft.Azure.Storage.OperationContext, System.IProgress<Microsoft.Azure.Storage.Core.Util.StorageProgress>, System.Threading.CancellationToken) in CloudBlockBlob.cs
    System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(System.Threading.Tasks.Task) in TaskAwaiter.cs
    System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(System.Threading.Tasks.Task) in TaskAwaiter.cs
    ...
    [Call Stack Truncated]
```
# MD5 のバリューがマッチしない

MD5のバリューがマッチしないと突然言われても「なんのMD５やねん」と思ってしまう。検索してみると、Blobのリファレンスを使いまわしているのが問題らしい。

* [StorageServerException MD5 value did not match when using CloudBlob.UploadFromStream](https://social.msdn.microsoft.com/forums/azure/en-us/4764e38f-b200-4efe-ada2-7de442dc4452/storageserverexception-md5-value-did-not-match-when-using-cloudblobuploadfromstream)

このMD５は何をしているか？というと、ファイルから、MD5を作成して、チェックサムとして使用している。[しばやん先生](https://blog.shibayan.jp/entry/20150302/1425260123)のブログにも記事があった。ファイルをアップロードすると、たまに壊れるケースがあるので、MD5でチェックサムを作って送る機能があるようす。

## エラーのバージョン

先の Exception が出たバージョンは次の通り。`cloudBlockBlob` は全く同じブロブなので、クラス変数にして、リファレンスを共有していた。

```csharp
        public async Task UploadScaleDecisionAsync(object obj)
        {
            var json = JsonConvert.SerializeObject(obj);
            await cloudBlockBlob.UploadTextAsync(json);
        }
```

上記のメソッドを呼び出す部分は、次のように、32並列で、同じ Blob を更新する。

```csharp
            Parallel.ForEach(Enumerable.Range(1, 32).ToArray(), async (idx) =>
            {
                while (true)
                {
                    await manager.UploadScaleDecisionAsync(new ScaleDecision(){Decision = "AddWorker", Index = idx, TimeStamp = DateTime.UtcNow});
                    await Task.Delay(TimeSpan.FromSeconds(1));
                }
            });
```

# デバッグで MD5 を観察する

コードを次のように修正して、デバッグをしてみる。

```csharp
        public async Task UploadScaleDecisionAsync(object obj)
        {
            var cloudBlockBlob = cloudBlobContainer.GetBlockBlobReference(Configuration.BlobName);
            var json = JsonConvert.SerializeObject(obj);
            await cloudBlockBlob.UploadTextAsync(json);
        }
```

MD5 が生成されている。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3470/0f2592c4-e5e2-cdd2-a816-07d303014fc5.png)

ドキュメントを見ると、MD5 のチェックサムはデフォルトで有効なので、あのエラーがでた原因もうなづける。下記に面白そうなオプションが定義できるようになっている。

* [BlobRequestOptions Class](https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.storage.blob.blobrequestoptions?view=azure-dotnet-legacy)

[BlobRequestOptions.DisableContentMD5Validation Property](https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.storage.blob.blobrequestoptions.disablecontentmd5validation?view=azure-dotnet-legacy)というオプションがあり、これがデフォルトで false なので、基本的にMD5のバリデーションをしてくれるのだろう。

おまけだが、リトライポリシーも設定できる。何も設定していなので、こちらの値がデフォルト値なのだろう。ただ、ドキュメントに記述がないので、将来にわたってそうであるかはわからない。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3470/92a66069-a138-536b-cee5-68898608c886.png)

# まとめ

Blobのリファレンスは同時更新するのであれば、シェアしてはいけない。リファレンスを無くすことで、Exception が出ず、期待通り後勝ちの動作をしてくれるようになった。

* [Sample: StorageAccountSpike](https://github.com/TsuyoshiUshio/StorageAccountSpike)