タイトル名のようなことが簡単に出来るのでは?と思いトライしてみたが、残念ながら、次のようなエラーをもらった。プログラム的には、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のバリューがマッチしないと突然言われても「なんのMD5やねん」と思ってしまう。検索してみると、Blobのリファレンスを使いまわしているのが問題らしい。
このMD5は何をしているか?というと、ファイルから、MD5を作成して、チェックサムとして使用している。しばやん先生のブログにも記事があった。ファイルをアップロードすると、たまに壊れるケースがあるので、MD5でチェックサムを作って送る機能があるようす。
エラーのバージョン
先の Exception が出たバージョンは次の通り。cloudBlockBlob は全く同じブロブなので、クラス変数にして、リファレンスを共有していた。
public async Task UploadScaleDecisionAsync(object obj)
{
var json = JsonConvert.SerializeObject(obj);
await cloudBlockBlob.UploadTextAsync(json);
}
上記のメソッドを呼び出す部分は、次のように、32並列で、同じ Blob を更新する。
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 を観察する
コードを次のように修正して、デバッグをしてみる。
public async Task UploadScaleDecisionAsync(object obj)
{
var cloudBlockBlob = cloudBlobContainer.GetBlockBlobReference(Configuration.BlobName);
var json = JsonConvert.SerializeObject(obj);
await cloudBlockBlob.UploadTextAsync(json);
}
ドキュメントを見ると、MD5 のチェックサムはデフォルトで有効なので、あのエラーがでた原因もうなづける。下記に面白そうなオプションが定義できるようになっている。
BlobRequestOptions.DisableContentMD5Validation Propertyというオプションがあり、これがデフォルトで false なので、基本的にMD5のバリデーションをしてくれるのだろう。
おまけだが、リトライポリシーも設定できる。何も設定していなので、こちらの値がデフォルト値なのだろう。ただ、ドキュメントに記述がないので、将来にわたってそうであるかはわからない。
まとめ
Blobのリファレンスは同時更新するのであれば、シェアしてはいけない。リファレンスを無くすことで、Exception が出ず、期待通り後勝ちの動作をしてくれるようになった。

