< Summary

Class:Azure.Extensions.AspNetCore.DataProtection.Blobs.AzureBlobXmlRepository
Assembly:Azure.Extensions.AspNetCore.DataProtection.Blobs
File(s):C:\Git\azure-sdk-for-net\sdk\extensions\Azure.Extensions.AspNetCore.DataProtection.Blobs\src\AzureBlobXmlRepository.cs
Covered lines:68
Uncovered lines:18
Coverable lines:86
Total lines:268
Line coverage:79% (68 of 86)
Covered branches:11
Total branches:20
Branch coverage:55% (11 of 20)

Metrics

MethodCyclomatic complexity Line coverage Branch coverage
.cctor()-100%100%
.ctor(...)-100%100%
GetAllElements()-0%100%
StoreElement(...)-75%50%
CreateDocumentFromBlob(...)-100%100%
GetAllElementsAsync()-0%0%
GetLatestDataAsync()-79.17%50%
GetRandomizedBackoffPeriod()-0%100%
StoreElementAsync()-93.94%80%

File(s)

C:\Git\azure-sdk-for-net\sdk\extensions\Azure.Extensions.AspNetCore.DataProtection.Blobs\src\AzureBlobXmlRepository.cs

#LineLine coverage
 1// Copyright (c) Microsoft Corporation. All rights reserved.
 2// Licensed under the MIT License.
 3
 4using System;
 5using System.Collections.Generic;
 6using System.Collections.ObjectModel;
 7using System.IO;
 8using System.Linq;
 9using System.Runtime.ExceptionServices;
 10using System.Threading;
 11using System.Threading.Tasks;
 12using System.Xml;
 13using System.Xml.Linq;
 14using Azure;
 15using Azure.Storage.Blobs;
 16using Azure.Storage.Blobs.Models;
 17using Microsoft.AspNetCore.DataProtection.Repositories;
 18
 19#pragma warning disable AZC0001 //
 20namespace Azure.Extensions.AspNetCore.DataProtection.Blobs
 21#pragma warning restore
 22{
 23    /// <summary>
 24    /// An <see cref="IXmlRepository"/> which is backed by Azure Blob Storage.
 25    /// </summary>
 26    /// <remarks>
 27    /// Instances of this type are thread-safe.
 28    /// </remarks>
 29    internal sealed class AzureBlobXmlRepository : IXmlRepository
 30    {
 31        private const int ConflictMaxRetries = 5;
 132        private static readonly TimeSpan ConflictBackoffPeriod = TimeSpan.FromMilliseconds(200);
 133        private static readonly XName RepositoryElementName = "repository";
 134        private static BlobHttpHeaders _blobHttpHeaders = new BlobHttpHeaders() { ContentType = "application/xml; charse
 35
 36        private readonly Random _random;
 37        private BlobData _cachedBlobData;
 38        private readonly BlobClient _blobClient;
 39
 40        /// <summary>
 41        /// Creates a new instance of the <see cref="AzureBlobXmlRepository"/>.
 42        /// </summary>
 43        /// <param name="blobClient">A <see cref="BlobClient"/> that is connected to the blob we are reading from and wr
 344        public AzureBlobXmlRepository(BlobClient blobClient)
 45        {
 346            _random = new Random();
 347            _blobClient = blobClient;
 348        }
 49
 50        /// <inheritdoc />
 51        public IReadOnlyCollection<XElement> GetAllElements()
 52        {
 53            // Shunt the work onto a ThreadPool thread so that it's independent of any
 54            // existing sync context or other potentially deadlock-causing items.
 55
 056            var elements = Task.Run(() => GetAllElementsAsync()).GetAwaiter().GetResult();
 057            return new ReadOnlyCollection<XElement>(elements);
 58        }
 59
 60        /// <inheritdoc />
 61        public void StoreElement(XElement element, string friendlyName)
 62        {
 263            if (element == null)
 64            {
 065                throw new ArgumentNullException(nameof(element));
 66            }
 67
 68            // Shunt the work onto a ThreadPool thread so that it's independent of any
 69            // existing sync context or other potentially deadlock-causing items.
 70
 471            Task.Run(() => StoreElementAsync(element)).GetAwaiter().GetResult();
 272        }
 73
 74        private XDocument CreateDocumentFromBlob(byte[] blob)
 75        {
 176            using (var memoryStream = new MemoryStream(blob))
 77            {
 178                var xmlReaderSettings = new XmlReaderSettings()
 179                {
 180                    DtdProcessing = DtdProcessing.Prohibit, IgnoreProcessingInstructions = true
 181                };
 82
 183                using (var xmlReader = XmlReader.Create(memoryStream, xmlReaderSettings))
 84                {
 185                    return XDocument.Load(xmlReader);
 86                }
 87            }
 188        }
 89
 90        private async Task<IList<XElement>> GetAllElementsAsync()
 91        {
 092            var data = await GetLatestDataAsync().ConfigureAwait(false);
 93
 094            if (data == null || data.BlobContents.Length == 0)
 95            {
 96                // no data in blob storage
 097                return Array.Empty<XElement>();
 98            }
 99
 100            // The document will look like this:
 101            //
 102            // <root>
 103            //   <child />
 104            //   <child />
 105            //   ...
 106            // </root>
 107            //
 108            // We want to return the first-level child elements to our caller.
 109
 0110            var doc = CreateDocumentFromBlob(data.BlobContents);
 0111            return doc.Root.Elements().ToList();
 0112        }
 113
 114        private async Task<BlobData> GetLatestDataAsync()
 115        {
 116            // Set the appropriate AccessCondition based on what we believe the latest
 117            // file contents to be, then make the request.
 118
 1119            var latestCachedData = Volatile.Read(ref _cachedBlobData); // local ref so field isn't mutated under our fee
 1120            var requestCondition = (latestCachedData != null)
 1121                ? new BlobRequestConditions() { IfNoneMatch = latestCachedData.ETag }
 1122                : null;
 123
 124            try
 125            {
 1126                using (var memoryStream = new MemoryStream())
 127                {
 1128                    var response = await _blobClient.DownloadToAsync(
 1129                        destination: memoryStream,
 1130                        conditions: requestCondition).ConfigureAwait(false);
 131
 1132                    if (response.Status == 304)
 133                    {
 134                        // 304 Not Modified
 135                        // Thrown when we already have the latest cached data.
 136                        // This isn't an error; we'll return our cached copy of the data.
 0137                        return latestCachedData;
 138                    }
 139
 140                    // At this point, our original cache either didn't exist or was outdated.
 141                    // We'll update it now and return the updated value
 1142                    latestCachedData = new BlobData()
 1143                    {
 1144                        BlobContents = memoryStream.ToArray(),
 1145                        ETag = response.Headers.ETag
 1146                    };
 147
 1148                }
 1149                Volatile.Write(ref _cachedBlobData, latestCachedData);
 1150            }
 0151            catch (RequestFailedException ex) when (ex.Status == 404)
 152            {
 153                // 404 Not Found
 154                // Thrown when no file exists in storage.
 155                // This isn't an error; we'll delete our cached copy of data.
 156
 0157                latestCachedData = null;
 0158                Volatile.Write(ref _cachedBlobData, latestCachedData);
 0159            }
 160
 1161            return latestCachedData;
 1162        }
 163
 164        private int GetRandomizedBackoffPeriod()
 165        {
 166            // returns a TimeSpan in the range [0.8, 1.0) * ConflictBackoffPeriod
 167            // not used for crypto purposes
 0168            var multiplier = 0.8 + (_random.NextDouble() * 0.2);
 0169            return (int) (multiplier * ConflictBackoffPeriod.Ticks);
 170        }
 171
 172        private async Task StoreElementAsync(XElement element)
 173        {
 174            // holds the last error in case we need to rethrow it
 2175            ExceptionDispatchInfo lastError = null;
 176
 6177            for (var i = 0; i < ConflictMaxRetries; i++)
 178            {
 3179                if (i > 1)
 180                {
 181                    // If multiple conflicts occurred, wait a small period of time before retrying
 182                    // the operation so that other writers can make forward progress.
 0183                    await Task.Delay(GetRandomizedBackoffPeriod()).ConfigureAwait(false);
 184                }
 185
 3186                if (i > 0)
 187                {
 188                    // If at least one conflict occurred, make sure we have an up-to-date
 189                    // view of the blob contents.
 1190                    await GetLatestDataAsync().ConfigureAwait(false);
 191                }
 192
 193                // Merge the new element into the document. If no document exists,
 194                // create a new default document and inject this element into it.
 195
 3196                var latestData = Volatile.Read(ref _cachedBlobData);
 3197                var doc = (latestData != null)
 3198                    ? CreateDocumentFromBlob(latestData.BlobContents)
 3199                    : new XDocument(new XElement(RepositoryElementName));
 3200                doc.Root.Add(element);
 201
 202                // Turn this document back into a byte[].
 203
 3204                var serializedDoc = new MemoryStream();
 3205                doc.Save(serializedDoc, SaveOptions.DisableFormatting);
 3206                serializedDoc.Position = 0;
 207
 208                // Generate the appropriate precondition header based on whether or not
 209                // we believe data already exists in storage.
 210
 211                BlobRequestConditions requestConditions;
 3212                if (latestData != null)
 213                {
 1214                    requestConditions = new BlobRequestConditions() { IfMatch = latestData.ETag };
 215                }
 216                else
 217                {
 2218                    requestConditions = new BlobRequestConditions() { IfNoneMatch = ETag.All };
 219                }
 220
 221                try
 222                {
 223                    // Send the request up to the server.
 3224                    var response = await _blobClient.UploadAsync(
 3225                        serializedDoc,
 3226                        httpHeaders: _blobHttpHeaders,
 3227                        conditions: requestConditions).ConfigureAwait(false);
 228
 229                    // If we got this far, success!
 230                    // We can update the cached view of the remote contents.
 231
 2232                    Volatile.Write(ref _cachedBlobData, new BlobData()
 2233                    {
 2234                        BlobContents = serializedDoc.ToArray(),
 2235                        ETag = response.Value.ETag // was updated by Upload routine
 2236                    });
 237
 2238                    return;
 239                }
 240                catch (RequestFailedException ex)
 1241                    when (ex.Status == 409 || ex.Status == 412)
 242                {
 243                    // 409 Conflict
 244                    // This error is rare but can be thrown in very special circumstances,
 245                    // such as if the blob in the process of being created. We treat it
 246                    // as equivalent to 412 for the purposes of retry logic.
 247
 248                    // 412 Precondition Failed
 249                    // We'll get this error if another writer updated the repository and we
 250                    // have an outdated view of its contents. If this occurs, we'll just
 251                    // refresh our view of the remote contents and try again up to the max
 252                    // retry limit.
 253
 1254                    lastError = ExceptionDispatchInfo.Capture(ex);
 1255                }
 1256            }
 257
 258            // if we got this far, something went awry
 0259            lastError.Throw();
 2260        }
 261
 262        private sealed class BlobData
 263        {
 264            internal byte[] BlobContents;
 265            internal ETag? ETag;
 266        }
 267    }
 268}