Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
36 changes: 30 additions & 6 deletions AutoNumber/BlobOptimisticDataStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

namespace AutoNumber
{
public class BlobOptimisticDataStore : IOptimisticDataStore
internal class BlobOptimisticDataStore : IOptimisticDataStore
{
private const string SeedValue = "1";
private readonly BlobContainerClient blobContainer;
Expand All @@ -33,24 +33,38 @@ public BlobOptimisticDataStore(BlobServiceClient blobServiceClient, IOptions<Aut
}

public string GetData(string blockName)
{
return GetDataWithConcurrencyCheck(blockName).Value;
}

public DataWrapper GetDataWithConcurrencyCheck(string blockName)
{
var blobReference = GetBlobReference(blockName);

using (var stream = new MemoryStream())
{
blobReference.DownloadTo(stream);
return Encoding.UTF8.GetString(stream.ToArray());
return new DataWrapper(Encoding.UTF8.GetString(stream.ToArray()),
blobReference.GetProperties().Value.ETag);
}
}

public async Task<string> GetDataAsync(string blockName)
{
var result = await GetDataWithConcurrencyCheckAsync(blockName);

return result.Value;
}

public async Task<DataWrapper> GetDataWithConcurrencyCheckAsync(string blockName)
{
var blobReference = GetBlobReference(blockName);

using (var stream = new MemoryStream())
{
await blobReference.DownloadToAsync(stream).ConfigureAwait(false);
return Encoding.UTF8.GetString(stream.ToArray());
var properties = await blobReference.GetPropertiesAsync();
return new DataWrapper(Encoding.UTF8.GetString(stream.ToArray()), properties.Value.ETag);
}
}

Expand All @@ -67,13 +81,18 @@ public bool Init()
}

public bool TryOptimisticWrite(string blockName, string data)
{
return TryOptimisticWriteWithConcurrencyCheck(blockName, data, Azure.ETag.All);
}

public bool TryOptimisticWriteWithConcurrencyCheck(string blockName, string data, Azure.ETag eTag)
{
var blobReference = GetBlobReference(blockName);
try
{
var blobRequestCondition = new BlobRequestConditions
{
IfMatch = (blobReference.GetProperties()).Value.ETag
IfMatch = eTag
};
UploadText(
blobReference,
Expand All @@ -92,13 +111,18 @@ public bool TryOptimisticWrite(string blockName, string data)
}

public async Task<bool> TryOptimisticWriteAsync(string blockName, string data)
{
return await TryOptimisticWriteWithConcurrencyCheckAsync(blockName, data, Azure.ETag.All);
}

public async Task<bool> TryOptimisticWriteWithConcurrencyCheckAsync(string blockName, string data, Azure.ETag eTag)
{
var blobReference = GetBlobReference(blockName);
try
{
var blobRequestCondition = new BlobRequestConditions
{
IfMatch = (await blobReference.GetPropertiesAsync()).Value.ETag
IfMatch = eTag
};
await UploadTextAsync(
blobReference,
Expand Down Expand Up @@ -135,7 +159,7 @@ private BlockBlobClient InitializeBlobReference(string blockName)
{
var blobRequestCondition = new BlobRequestConditions
{
IfNoneMatch = ETag.All
IfNoneMatch = Azure.ETag.All
};
UploadText(blobReference, SeedValue, blobRequestCondition);
}
Expand Down
15 changes: 15 additions & 0 deletions AutoNumber/DataWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace AutoNumber
{
public class DataWrapper
{
public DataWrapper(string value, Azure.ETag eTag)
{
Value = value;
ETag = eTag;
}

public Azure.ETag ETag { get; private set; }

public string Value { get; private set; }
}
}
40 changes: 37 additions & 3 deletions AutoNumber/DebugOnlyFileDataStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

namespace AutoNumber
{
public class DebugOnlyFileDataStore : IOptimisticDataStore
internal class DebugOnlyFileDataStore : IOptimisticDataStore
{
private const string SeedValue = "1";

Expand All @@ -17,11 +17,17 @@ public DebugOnlyFileDataStore(string directoryPath)
}

public string GetData(string blockName)
{
return GetDataWithConcurrencyCheck(blockName).Value;
}

public DataWrapper GetDataWithConcurrencyCheck(string blockName)
{
var blockPath = Path.Combine(directoryPath, $"{blockName}.txt");
try
{
return File.ReadAllText(blockPath);
var info = new FileInfo(blockPath);
return new DataWrapper(File.ReadAllText(blockPath), ETag.ForDate(info.LastWriteTimeUtc));
}
catch (FileNotFoundException)
{
Expand All @@ -32,7 +38,9 @@ public string GetData(string blockName)
streamWriter.Write(SeedValue);
}

return SeedValue;
var info = new FileInfo(blockPath);

return new DataWrapper(SeedValue, ETag.ForDate(info.LastWriteTimeUtc));
}
}

Expand All @@ -41,6 +49,11 @@ public Task<string> GetDataAsync(string blockName)
throw new NotImplementedException();
}

public Task<DataWrapper> GetDataWithConcurrencyCheckAsync(string blockName)
{
throw new NotImplementedException();
}

public Task<bool> InitAsync()
{
return Task.FromResult(true);
Expand All @@ -52,8 +65,24 @@ public bool Init()
}

public bool TryOptimisticWrite(string blockName, string data)
{
return TryOptimisticWriteWithConcurrencyCheck(blockName, data, Azure.ETag.All);
}

public bool TryOptimisticWriteWithConcurrencyCheck(string blockName, string data, Azure.ETag eTag)
{
var blockPath = Path.Combine(directoryPath, $"{blockName}.txt");

if(!File.Exists(blockPath)) return false;

if (!eTag.Equals(Azure.ETag.All))
{
var info = new FileInfo(blockPath);
var eTagToCompare = ETag.ForDate(info.LastWriteTimeUtc);

if (!eTagToCompare.Equals(eTag)) return false;
}

File.WriteAllText(blockPath, data);
return true;
}
Expand All @@ -62,5 +91,10 @@ public Task<bool> TryOptimisticWriteAsync(string blockName, string data)
{
throw new NotImplementedException();
}

public Task<bool> TryOptimisticWriteWithConcurrencyCheckAsync(string blockName, string data, Azure.ETag eTag)
{
throw new NotImplementedException();
}
}
}
12 changes: 12 additions & 0 deletions AutoNumber/ETag.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System;

namespace AutoNumber
{
internal static class ETag
{
public static Azure.ETag ForDate(DateTime input)
{
return new Azure.ETag($"W/\"{input.Ticks}\"");
}
}
}
12 changes: 11 additions & 1 deletion AutoNumber/Interfaces/IOptimisticDataStore.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
using System.Threading.Tasks;
using System.Diagnostics.Tracing;
using System.Threading.Tasks;
using Azure;

namespace AutoNumber.Interfaces
{
public interface IOptimisticDataStore
{
string GetData(string blockName);
DataWrapper GetDataWithConcurrencyCheck(string blockName);

Task<string> GetDataAsync(string blockName);
Task<DataWrapper> GetDataWithConcurrencyCheckAsync(string blockName);

bool TryOptimisticWrite(string blockName, string data);
bool TryOptimisticWriteWithConcurrencyCheck(string blockName, string data, Azure.ETag eTag);

Task<bool> TryOptimisticWriteAsync(string blockName, string data);

Task<bool> TryOptimisticWriteWithConcurrencyCheckAsync(string blockName, string data, Azure.ETag eTag);
Task<bool> InitAsync();
bool Init();
}
Expand Down
7 changes: 7 additions & 0 deletions AutoNumber/InternalsVisibleTo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using System;
using System.IO;
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("AutoNumber.UnitTests")]
[assembly: InternalsVisibleTo("AutoNumber.IntegrationTests")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
10 changes: 5 additions & 5 deletions AutoNumber/UniqueIdGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ namespace AutoNumber
/// <summary>
/// Generate a new incremental id regards the scope name
/// </summary>
public class UniqueIdGenerator : IUniqueIdGenerator
internal class UniqueIdGenerator : IUniqueIdGenerator
{
/// <summary>
/// Generate a new incremental id regards the scope name
Expand Down Expand Up @@ -47,18 +47,18 @@ private void UpdateFromSyncStore(string scopeName, ScopeState state)

while (writesAttempted < MaxWriteAttempts)
{
var data = optimisticDataStore.GetData(scopeName);
var data = optimisticDataStore.GetDataWithConcurrencyCheck(scopeName);

if (!long.TryParse(data, out var nextId))
if (!long.TryParse(data.Value, out var nextId))
throw new UniqueIdGenerationException(
$"The id seed returned from storage for scope '{scopeName}' was corrupt, and could not be parsed as a long. The data returned was: {data}");

state.LastId = nextId - 1;
state.HighestIdAvailableInBatch = nextId - 1 + BatchSize;
var firstIdInNextBatch = state.HighestIdAvailableInBatch + 1;

if (optimisticDataStore.TryOptimisticWrite(scopeName,
firstIdInNextBatch.ToString(CultureInfo.InvariantCulture)))
if (optimisticDataStore.TryOptimisticWriteWithConcurrencyCheck(scopeName,
firstIdInNextBatch.ToString(CultureInfo.InvariantCulture), data.ETag))
return;

writesAttempted++;
Expand Down
6 changes: 3 additions & 3 deletions IntegrationTests/Azure.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,19 @@
using Azure.Storage.Blobs.Specialized;
using NUnit.Framework;

namespace IntegrationTests.cs
namespace AutoNumber.IntegrationTests
{
[TestFixture]
public class Azure : Scenarios<TestScope>
{
private readonly BlobServiceClient blobServiceClient = new BlobServiceClient("UseDevelopmentStorage=true");

protected override TestScope BuildTestScope()
internal override TestScope BuildTestScope()
{
return new TestScope(new BlobServiceClient("UseDevelopmentStorage=true"));
}

protected override IOptimisticDataStore BuildStore(TestScope scope)
internal override IOptimisticDataStore BuildStore(TestScope scope)
{
var blobOptimisticDataStore = new BlobOptimisticDataStore(blobServiceClient, scope.ContainerName);
blobOptimisticDataStore.Init();
Expand Down
6 changes: 3 additions & 3 deletions IntegrationTests/File.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@
using AutoNumber.Interfaces;
using NUnit.Framework;

namespace IntegrationTests.cs
namespace AutoNumber.IntegrationTests
{
[TestFixture]
public class File : Scenarios<File.TestScope>
{
protected override TestScope BuildTestScope()
internal override TestScope BuildTestScope()
{
return new TestScope();
}

protected override IOptimisticDataStore BuildStore(TestScope scope)
internal override IOptimisticDataStore BuildStore(TestScope scope)
{
return new DebugOnlyFileDataStore(scope.DirectoryPath);
}
Expand Down
2 changes: 1 addition & 1 deletion IntegrationTests/ITestScope.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System;

namespace IntegrationTests.cs
namespace AutoNumber.IntegrationTests
{
public interface ITestScope : IDisposable
{
Expand Down
4 changes: 2 additions & 2 deletions IntegrationTests/IntegrationTests.csproj
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<ProductVersion>8.0.30703</ProductVersion>
<RootNamespace>IntegrationTests.cs</RootNamespace>
<AssemblyName>IntegrationTests.cs</AssemblyName>
<RootNamespace>AutoNumber.IntegrationTests</RootNamespace>
<AssemblyName>AutoNumber.IntegrationTests</AssemblyName>
<SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir>
<AssemblyTitle>IntegrationTests.cs</AssemblyTitle>
<Company>Microsoft</Company>
Expand Down
6 changes: 3 additions & 3 deletions IntegrationTests/Scenarios.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
using AutoNumber.Interfaces;
using NUnit.Framework;

namespace IntegrationTests.cs
namespace AutoNumber.IntegrationTests
{
public abstract class Scenarios<TTestScope> where TTestScope : ITestScope
{
protected abstract IOptimisticDataStore BuildStore(TTestScope scope);
protected abstract TTestScope BuildTestScope();
internal abstract IOptimisticDataStore BuildStore(TTestScope scope);
internal abstract TTestScope BuildTestScope();

[Test]
public void ShouldReturnOneForFirstIdInNewScope()
Expand Down
Loading