Skip to content
Closed
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions THIRD-PARTY-NOTICES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -189,3 +189,27 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

License notice for azure-sdk-for-net
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-------------------------------------------
The MIT License (MIT)

Copyright (c) 2015 Microsoft

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
2 changes: 2 additions & 0 deletions eng/Dependencies.props
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ and are generated based on the last package release.

<ItemGroup Label="External dependencies" Condition="'$(DotNetBuildFromSource)' != 'true'">
<LatestPackageReference Include="AngleSharp" Version="$(AngleSharpPackageVersion)" />
<LatestPackageReference Include="Azure.Storage.Blobs" Version="$(AzureStorageBlobPackageVersion)" />
<LatestPackageReference Include="Azure.Identity" Version="$(AzureIdentityPackageVersion)" />
<LatestPackageReference Include="BenchmarkDotNet" Version="$(BenchmarkDotNetPackageVersion)" />
<LatestPackageReference Include="FSharp.Core" Version="$(FSharpCorePackageVersion)" />
<LatestPackageReference Include="Google.ProtoBuf" Version="$(GoogleProtoBufPackageVersion)" />
Expand Down
1 change: 1 addition & 0 deletions eng/ProjectReferences.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<Project>
<ItemGroup>
<ProjectReferenceProvider Include="Microsoft.AspNetCore.JsonPatch" ProjectPath="$(RepoRoot)src\Features\JsonPatch\src\Microsoft.AspNetCore.JsonPatch.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.DataProtection.Azure.Storage.Blob" ProjectPath="$(RepoRoot)src\DataProtection\Azure.Storage.Blob\src\Microsoft.AspNetCore.DataProtection.Azure.Storage.Blob.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.DataProtection.AzureKeyVault" ProjectPath="$(RepoRoot)src\DataProtection\AzureKeyVault\src\Microsoft.AspNetCore.DataProtection.AzureKeyVault.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.DataProtection.AzureStorage" ProjectPath="$(RepoRoot)src\DataProtection\AzureStorage\src\Microsoft.AspNetCore.DataProtection.AzureStorage.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.DataProtection.EntityFrameworkCore" ProjectPath="$(RepoRoot)src\DataProtection\EntityFrameworkCore\src\Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.csproj" />
Expand Down
3 changes: 3 additions & 0 deletions eng/Versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<!-- Blazor Client packages will not RTM with 3.1 -->
<BlazorClientPreReleasePreviewNumber>4</BlazorClientPreReleasePreviewNumber>
<BlazorClientPreReleaseVersionLabel>preview$(BlazorClientPreReleasePreviewNumber)</BlazorClientPreReleaseVersionLabel>
<AzureSDKPreReleaseVersionLabel>preview1</AzureSDKPreReleaseVersionLabel>
<AspNetCoreMajorMinorVersion>$(AspNetCoreMajorVersion).$(AspNetCoreMinorVersion)</AspNetCoreMajorMinorVersion>
<!-- The following property may need to be updated if ingesting new versions of Extensions.Refs package. The package override version is used to create PackageOverrides.txt in the targeting pack. -->
<MicrosoftInternalExtensionsRefsPackageOverrideVersion>3.1.0</MicrosoftInternalExtensionsRefsPackageOverrideVersion>
Expand Down Expand Up @@ -233,6 +234,8 @@
<MicrosoftAspNetCoreAzureAppServicesSiteExtension22PackageVersion>2.2.0</MicrosoftAspNetCoreAzureAppServicesSiteExtension22PackageVersion>
<!-- 3rd party dependencies -->
<AngleSharpPackageVersion>0.9.9</AngleSharpPackageVersion>
<AzureStorageBlobPackageVersion>12.1.0</AzureStorageBlobPackageVersion>
<AzureIdentityPackageVersion>1.0.0</AzureIdentityPackageVersion>
<BenchmarkDotNetPackageVersion>0.10.13</BenchmarkDotNetPackageVersion>
<CastleCorePackageVersion>4.2.1</CastleCorePackageVersion>
<FSharpCorePackageVersion>4.2.1</FSharpCorePackageVersion>
Expand Down
267 changes: 267 additions & 0 deletions src/DataProtection/Azure.Storage.Blob/src/AzureBlobXmlRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;
using Azure;
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
using Microsoft.AspNetCore.DataProtection.Repositories;

namespace Microsoft.AspNetCore.DataProtection.Azure.Storage.Blob
{
/// <summary>
/// An <see cref="IXmlRepository"/> which is backed by Azure Blob Storage.
/// </summary>
/// <remarks>
/// Instances of this type are thread-safe.
/// </remarks>
public sealed class AzureBlobXmlRepository : IXmlRepository
{
private const int ConflictMaxRetries = 5;
private static readonly TimeSpan ConflictBackoffPeriod = TimeSpan.FromMilliseconds(200);

private static readonly XName RepositoryElementName = "repository";

private readonly Random _random;
private BlobData _cachedBlobData;
private readonly BlobClient _blobClient;

/// <summary>
/// Creates a new instance of the <see cref="AzureBlobXmlRepository"/>.
/// </summary>
/// <param name="blobClient">A <see cref="BlobClient"/> that is connected to the blob we are reading from and writing to.</param>
public AzureBlobXmlRepository(BlobClient blobClient)
{
_random = new Random();
_blobClient = blobClient;
}

/// <inheritdoc />
public IReadOnlyCollection<XElement> GetAllElements()
{
// Shunt the work onto a ThreadPool thread so that it's independent of any
// existing sync context or other potentially deadlock-causing items.

var elements = Task.Run(() => GetAllElementsAsync()).GetAwaiter().GetResult();
return new ReadOnlyCollection<XElement>(elements);
}

/// <inheritdoc />
public void StoreElement(XElement element, string friendlyName)
{
if (element == null)
{
throw new ArgumentNullException(nameof(element));
}

// Shunt the work onto a ThreadPool thread so that it's independent of any
// existing sync context or other potentially deadlock-causing items.

Task.Run(() => StoreElementAsync(element)).GetAwaiter().GetResult();
}

private XDocument CreateDocumentFromBlob(byte[] blob)
{
using (var memoryStream = new MemoryStream(blob))
{
var xmlReaderSettings = new XmlReaderSettings()
{
DtdProcessing = DtdProcessing.Prohibit, IgnoreProcessingInstructions = true
};

using (var xmlReader = XmlReader.Create(memoryStream, xmlReaderSettings))
{
return XDocument.Load(xmlReader);
}
}
}

private async Task<IList<XElement>> GetAllElementsAsync()
{
var data = await GetLatestDataAsync();

if (data == null || data.BlobContents.Length == 0)
{
// no data in blob storage
return new XElement[0];
}

// The document will look like this:
//
// <root>
// <child />
// <child />
// ...
// </root>
//
// We want to return the first-level child elements to our caller.

var doc = CreateDocumentFromBlob(data.BlobContents);
return doc.Root.Elements().ToList();
}

private async Task<BlobData> GetLatestDataAsync()
{
// Set the appropriate AccessCondition based on what we believe the latest
// file contents to be, then make the request.

var latestCachedData = Volatile.Read(ref _cachedBlobData); // local ref so field isn't mutated under our feet
var requestCondition = (latestCachedData != null)
? new BlobRequestConditions() { IfNoneMatch = latestCachedData.ETag }
: null;

try
{
using (var memoryStream = new MemoryStream())
{
var response = await _blobClient.DownloadToAsync(
destination: memoryStream,
conditions: requestCondition);

// At this point, our original cache either didn't exist or was outdated.
// We'll update it now and return the updated value
latestCachedData = new BlobData()
{
BlobContents = memoryStream.ToArray(),
ETag = response.Headers.ETag
};

}
Volatile.Write(ref _cachedBlobData, latestCachedData);
}
catch (RequestFailedException ex) when (ex.Status == 304)
{
// 304 Not Modified
// Thrown when we already have the latest cached data.
// This isn't an error; we'll return our cached copy of the data.
}
catch (RequestFailedException ex) when (ex.Status == 404)
{
// 404 Not Found
// Thrown when no file exists in storage.
// This isn't an error; we'll delete our cached copy of data.

latestCachedData = null;
Volatile.Write(ref _cachedBlobData, latestCachedData);
}

return latestCachedData;
}

private int GetRandomizedBackoffPeriod()
{
// returns a TimeSpan in the range [0.8, 1.0) * ConflictBackoffPeriod
// not used for crypto purposes
var multiplier = 0.8 + (_random.NextDouble() * 0.2);
return (int) (multiplier * ConflictBackoffPeriod.Ticks);
}

private async Task StoreElementAsync(XElement element)
{
// holds the last error in case we need to rethrow it
ExceptionDispatchInfo lastError = null;

for (var i = 0; i < ConflictMaxRetries; i++)
{
if (i > 1)
{
// If multiple conflicts occurred, wait a small period of time before retrying
// the operation so that other writers can make forward progress.
await Task.Delay(GetRandomizedBackoffPeriod());
}

if (i > 0)
{
// If at least one conflict occurred, make sure we have an up-to-date
// view of the blob contents.
await GetLatestDataAsync();
}

// Merge the new element into the document. If no document exists,
// create a new default document and inject this element into it.

var latestData = Volatile.Read(ref _cachedBlobData);
var doc = (latestData != null)
? CreateDocumentFromBlob(latestData.BlobContents)
: new XDocument(new XElement(RepositoryElementName));
doc.Root.Add(element);

// Turn this document back into a byte[].

var serializedDoc = new MemoryStream();
doc.Save(serializedDoc, SaveOptions.DisableFormatting);
serializedDoc.Position = 0;

// Generate the appropriate precondition header based on whether or not
// we believe data already exists in storage.

BlobRequestConditions requestConditions;
BlobHttpHeaders headers = null;
if (latestData != null)
{
requestConditions = new BlobRequestConditions() { IfMatch = latestData.ETag };
}
else
{
requestConditions = new BlobRequestConditions() { IfNoneMatch = ETag.All };
// set content type on first write
headers = new BlobHttpHeaders() { ContentType = "application/xml; charset=utf-8" };
}

try
{
// Send the request up to the server.
var response = await _blobClient.UploadAsync(
serializedDoc,
httpHeaders: headers,
conditions: requestConditions);

// If we got this far, success!
// We can update the cached view of the remote contents.

Volatile.Write(ref _cachedBlobData, new BlobData()
{
BlobContents = serializedDoc.ToArray(),
ETag = response.Value.ETag // was updated by Upload routine
});

return;
}
catch (RequestFailedException ex)
when (ex.Status == 409 || ex.Status == 412)
{
// 409 Conflict
// This error is rare but can be thrown in very special circumstances,
// such as if the blob in the process of being created. We treat it
// as equivalent to 412 for the purposes of retry logic.

// 412 Precondition Failed
// We'll get this error if another writer updated the repository and we
// have an outdated view of its contents. If this occurs, we'll just
// refresh our view of the remote contents and try again up to the max
// retry limit.

lastError = ExceptionDispatchInfo.Capture(ex);
}
}

// if we got this far, something went awry
lastError.Throw();
}

private sealed class BlobData
{
internal byte[] BlobContents;
internal ETag? ETag;
}
}
}
Loading