From addd12b64dff99e1e313a9c48e2dd2d2e5fbbcea Mon Sep 17 00:00:00 2001 From: Bart Meirens Date: Mon, 16 Dec 2024 13:32:55 +0100 Subject: [PATCH 1/3] Add eTags on read and write operations to guard against concurrency over different instances --- AutoNumber/BlobOptimisticDataStore.cs | 20 +++++++------ AutoNumber/DebugOnlyFileDataStore.cs | 17 +++++++---- AutoNumber/ETag.cs | 12 ++++++++ AutoNumber/Interfaces/IOptimisticDataStore.cs | 22 +++++++++++--- AutoNumber/InternalsVisibleTo.cs | 5 ++++ AutoNumber/UniqueIdGenerator.cs | 4 +-- UnitTests/UniqueIdGeneratorTest.cs | 30 +++++++++++++------ 7 files changed, 80 insertions(+), 30 deletions(-) create mode 100644 AutoNumber/ETag.cs create mode 100644 AutoNumber/InternalsVisibleTo.cs diff --git a/AutoNumber/BlobOptimisticDataStore.cs b/AutoNumber/BlobOptimisticDataStore.cs index f0e5f1e..1673857 100644 --- a/AutoNumber/BlobOptimisticDataStore.cs +++ b/AutoNumber/BlobOptimisticDataStore.cs @@ -32,25 +32,27 @@ public BlobOptimisticDataStore(BlobServiceClient blobServiceClient, IOptions GetDataAsync(string blockName) + public async Task GetDataAsync(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); } } @@ -66,14 +68,14 @@ public bool Init() return result == null || result.Value != null; } - public bool TryOptimisticWrite(string blockName, string data) + public bool TryOptimisticWrite(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, @@ -91,14 +93,14 @@ public bool TryOptimisticWrite(string blockName, string data) return true; } - public async Task TryOptimisticWriteAsync(string blockName, string data) + public async Task TryOptimisticWriteAsync(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, @@ -135,7 +137,7 @@ private BlockBlobClient InitializeBlobReference(string blockName) { var blobRequestCondition = new BlobRequestConditions { - IfNoneMatch = ETag.All + IfNoneMatch = Azure.ETag.All }; UploadText(blobReference, SeedValue, blobRequestCondition); } diff --git a/AutoNumber/DebugOnlyFileDataStore.cs b/AutoNumber/DebugOnlyFileDataStore.cs index 0c93bfd..59b12f3 100644 --- a/AutoNumber/DebugOnlyFileDataStore.cs +++ b/AutoNumber/DebugOnlyFileDataStore.cs @@ -1,7 +1,9 @@ using System; using System.IO; using System.Threading.Tasks; +using System.Xml.Linq; using AutoNumber.Interfaces; +using Azure; namespace AutoNumber { @@ -16,12 +18,13 @@ public DebugOnlyFileDataStore(string directoryPath) this.directoryPath = directoryPath; } - public string GetData(string blockName) + public DataWrapper GetData(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) { @@ -32,11 +35,13 @@ public string GetData(string blockName) streamWriter.Write(SeedValue); } - return SeedValue; + var info = new FileInfo(blockPath); + + return new DataWrapper(SeedValue, ETag.ForDate(info.LastWriteTimeUtc)); } } - public Task GetDataAsync(string blockName) + public Task GetDataAsync(string blockName) { throw new NotImplementedException(); } @@ -51,14 +56,14 @@ public bool Init() return true; } - public bool TryOptimisticWrite(string blockName, string data) + public bool TryOptimisticWrite(string blockName, string data, Azure.ETag eTag) { var blockPath = Path.Combine(directoryPath, $"{blockName}.txt"); File.WriteAllText(blockPath, data); return true; } - public Task TryOptimisticWriteAsync(string blockName, string data) + public Task TryOptimisticWriteAsync(string blockName, string data, Azure.ETag eTag) { throw new NotImplementedException(); } diff --git a/AutoNumber/ETag.cs b/AutoNumber/ETag.cs new file mode 100644 index 0000000..2a97ff5 --- /dev/null +++ b/AutoNumber/ETag.cs @@ -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}\""); + } + } +} \ No newline at end of file diff --git a/AutoNumber/Interfaces/IOptimisticDataStore.cs b/AutoNumber/Interfaces/IOptimisticDataStore.cs index e201cda..100e28d 100644 --- a/AutoNumber/Interfaces/IOptimisticDataStore.cs +++ b/AutoNumber/Interfaces/IOptimisticDataStore.cs @@ -1,14 +1,28 @@ using System.Threading.Tasks; +using Azure; namespace AutoNumber.Interfaces { public interface IOptimisticDataStore { - string GetData(string blockName); - Task GetDataAsync(string blockName); - bool TryOptimisticWrite(string blockName, string data); - Task TryOptimisticWriteAsync(string blockName, string data); + DataWrapper GetData(string blockName); + Task GetDataAsync(string blockName); + bool TryOptimisticWrite(string blockName, string data, Azure.ETag eTag); + Task TryOptimisticWriteAsync(string blockName, string data, Azure.ETag eTag); Task InitAsync(); bool Init(); } + + 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; } + } } \ No newline at end of file diff --git a/AutoNumber/InternalsVisibleTo.cs b/AutoNumber/InternalsVisibleTo.cs new file mode 100644 index 0000000..ab2f090 --- /dev/null +++ b/AutoNumber/InternalsVisibleTo.cs @@ -0,0 +1,5 @@ +using System; +using System.IO; +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("AutoNumber.UnitTests")] \ No newline at end of file diff --git a/AutoNumber/UniqueIdGenerator.cs b/AutoNumber/UniqueIdGenerator.cs index 765fa88..f21f00e 100644 --- a/AutoNumber/UniqueIdGenerator.cs +++ b/AutoNumber/UniqueIdGenerator.cs @@ -49,7 +49,7 @@ private void UpdateFromSyncStore(string scopeName, ScopeState state) { var data = optimisticDataStore.GetData(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}"); @@ -58,7 +58,7 @@ private void UpdateFromSyncStore(string scopeName, ScopeState state) var firstIdInNextBatch = state.HighestIdAvailableInBatch + 1; if (optimisticDataStore.TryOptimisticWrite(scopeName, - firstIdInNextBatch.ToString(CultureInfo.InvariantCulture))) + firstIdInNextBatch.ToString(CultureInfo.InvariantCulture), data.ETag)) return; writesAttempted++; diff --git a/UnitTests/UniqueIdGeneratorTest.cs b/UnitTests/UniqueIdGeneratorTest.cs index 011a4ff..7f3d33a 100644 --- a/UnitTests/UniqueIdGeneratorTest.cs +++ b/UnitTests/UniqueIdGeneratorTest.cs @@ -1,6 +1,7 @@ using System; using AutoNumber.Exceptions; using AutoNumber.Interfaces; +using Azure; using NSubstitute; using NUnit.Framework; using NUnit.Framework.Legacy; @@ -48,9 +49,14 @@ public void MaxWriteAttemptsShouldThrowArgumentOutOfRangeExceptionWhenValueIsZer [Test] public void NextIdShouldReturnNumbersSequentially() { + var eTagDate = new DateTime(2000, 1, 2, 3, 4, 5, 6, DateTimeKind.Utc); var store = Substitute.For(); - store.GetData("test").Returns("0", "250"); - store.TryOptimisticWrite("test", "3").Returns(true); + store.GetData("test") + .Returns( + new DataWrapper("0", ETag.ForDate(eTagDate)), + new DataWrapper("250", ETag.ForDate(eTagDate.AddMonths(1)))); + store.TryOptimisticWrite("test", "3", ETag.ForDate(eTagDate)) + .Returns(true); var subject = new UniqueIdGenerator(store) { @@ -65,10 +71,14 @@ public void NextIdShouldReturnNumbersSequentially() [Test] public void NextIdShouldRollOverToNewBlockWhenCurrentBlockIsExhausted() { + var eTagDate = new DateTime(2000, 1, 2, 3, 4, 5, 6, DateTimeKind.Utc); + var eTagDate2 = eTagDate.AddMonths(1); var store = Substitute.For(); - store.GetData("test").Returns("0", "250"); - store.TryOptimisticWrite("test", "3").Returns(true); - store.TryOptimisticWrite("test", "253").Returns(true); + store.GetData("test").Returns( + new DataWrapper("0", ETag.ForDate(eTagDate)), + new DataWrapper("250", ETag.ForDate(eTagDate2))); + store.TryOptimisticWrite("test", "3", ETag.ForDate(eTagDate)).Returns(true); + store.TryOptimisticWrite("test", "253", ETag.ForDate(eTagDate2)).Returns(true); var subject = new UniqueIdGenerator(store) { @@ -87,7 +97,7 @@ public void NextIdShouldRollOverToNewBlockWhenCurrentBlockIsExhausted() public void NextIdShouldThrowExceptionOnCorruptData() { var store = Substitute.For(); - store.GetData("test").Returns("abc"); + store.GetData("test").Returns(new DataWrapper("abc", Azure.ETag.All)); Assert.That(() => { @@ -101,7 +111,7 @@ public void NextIdShouldThrowExceptionOnCorruptData() public void NextIdShouldThrowExceptionOnNullData() { var store = Substitute.For(); - store.GetData("test").Returns((string) null); + store.GetData("test").Returns(new DataWrapper((string)null, Azure.ETag.All)); Assert.That(() => { @@ -114,9 +124,11 @@ public void NextIdShouldThrowExceptionOnNullData() [Test] public void NextIdShouldThrowExceptionWhenRetriesAreExhausted() { + var eTagDate = new DateTime(2000, 1, 2, 3, 4, 5, 6, DateTimeKind.Utc); var store = Substitute.For(); - store.GetData("test").Returns("0"); - store.TryOptimisticWrite("test", "3").Returns(false, false, false, true); + store.GetData("test").Returns(new DataWrapper("0", ETag.ForDate(eTagDate))); + store.TryOptimisticWrite("test", "3", ETag.ForDate(eTagDate.AddMonths(1))) + .Returns(false, false, false, true); var generator = new UniqueIdGenerator(store) { From 6d3a2a3acdc748180c22b67686c97f5ddf621ac5 Mon Sep 17 00:00:00 2001 From: Bart Meirens Date: Mon, 16 Dec 2024 13:48:47 +0100 Subject: [PATCH 2/3] No breaking interface change for eTag support --- AutoNumber/BlobOptimisticDataStore.cs | 8 ++++---- AutoNumber/DebugOnlyFileDataStore.cs | 16 ++++++++++++++-- AutoNumber/Interfaces/IOptimisticDataStore.cs | 7 ++++--- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/AutoNumber/BlobOptimisticDataStore.cs b/AutoNumber/BlobOptimisticDataStore.cs index 1673857..aed2740 100644 --- a/AutoNumber/BlobOptimisticDataStore.cs +++ b/AutoNumber/BlobOptimisticDataStore.cs @@ -68,14 +68,14 @@ public bool Init() return result == null || result.Value != null; } - public bool TryOptimisticWrite(string blockName, string data, Azure.ETag eTag) + public bool TryOptimisticWrite(string blockName, string data, Azure.ETag? eTag) { var blobReference = GetBlobReference(blockName); try { var blobRequestCondition = new BlobRequestConditions { - IfMatch = eTag + IfMatch = eTag ?? (blobReference.GetProperties()).Value.ETag }; UploadText( blobReference, @@ -93,14 +93,14 @@ public bool TryOptimisticWrite(string blockName, string data, Azure.ETag eTag) return true; } - public async Task TryOptimisticWriteAsync(string blockName, string data, Azure.ETag eTag) + public async Task TryOptimisticWriteAsync(string blockName, string data, Azure.ETag? eTag) { var blobReference = GetBlobReference(blockName); try { var blobRequestCondition = new BlobRequestConditions { - IfMatch = eTag + IfMatch = eTag ?? (await blobReference.GetPropertiesAsync()).Value.ETag }; await UploadTextAsync( blobReference, diff --git a/AutoNumber/DebugOnlyFileDataStore.cs b/AutoNumber/DebugOnlyFileDataStore.cs index 59b12f3..77e8e9c 100644 --- a/AutoNumber/DebugOnlyFileDataStore.cs +++ b/AutoNumber/DebugOnlyFileDataStore.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Runtime.CompilerServices; using System.Threading.Tasks; using System.Xml.Linq; using AutoNumber.Interfaces; @@ -56,14 +57,25 @@ public bool Init() return true; } - public bool TryOptimisticWrite(string blockName, string data, Azure.ETag eTag) + public bool TryOptimisticWrite(string blockName, string data, Azure.ETag? eTag) { var blockPath = Path.Combine(directoryPath, $"{blockName}.txt"); + + if (eTag.HasValue) + { + if(!File.Exists(blockPath)) return false; + + var info = new FileInfo(blockPath); + var eTagToCompare = ETag.ForDate(info.LastWriteTimeUtc); + + if (!eTagToCompare.Equals(eTag.Value)) return false; + } + File.WriteAllText(blockPath, data); return true; } - public Task TryOptimisticWriteAsync(string blockName, string data, Azure.ETag eTag) + public Task TryOptimisticWriteAsync(string blockName, string data, Azure.ETag? eTag) { throw new NotImplementedException(); } diff --git a/AutoNumber/Interfaces/IOptimisticDataStore.cs b/AutoNumber/Interfaces/IOptimisticDataStore.cs index 100e28d..ea8cc7c 100644 --- a/AutoNumber/Interfaces/IOptimisticDataStore.cs +++ b/AutoNumber/Interfaces/IOptimisticDataStore.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Diagnostics.Tracing; +using System.Threading.Tasks; using Azure; namespace AutoNumber.Interfaces @@ -7,8 +8,8 @@ public interface IOptimisticDataStore { DataWrapper GetData(string blockName); Task GetDataAsync(string blockName); - bool TryOptimisticWrite(string blockName, string data, Azure.ETag eTag); - Task TryOptimisticWriteAsync(string blockName, string data, Azure.ETag eTag); + bool TryOptimisticWrite(string blockName, string data, Azure.ETag? eTag); + Task TryOptimisticWriteAsync(string blockName, string data, Azure.ETag? eTag); Task InitAsync(); bool Init(); } From 9e2bcd19adb13eb35c21bbea7372fb71e5d097e9 Mon Sep 17 00:00:00 2001 From: Bart Meirens Date: Mon, 16 Dec 2024 21:02:46 +0100 Subject: [PATCH 3/3] Backwards compatible interface definition; Less leaky implementations --- AutoNumber/BlobOptimisticDataStore.cs | 36 ++++++++++++---- AutoNumber/DataWrapper.cs | 15 +++++++ AutoNumber/DebugOnlyFileDataStore.cs | 41 +++++++++++++------ AutoNumber/Interfaces/IOptimisticDataStore.cs | 27 +++++------- AutoNumber/InternalsVisibleTo.cs | 4 +- AutoNumber/UniqueIdGenerator.cs | 6 +-- IntegrationTests/Azure.cs | 6 +-- IntegrationTests/File.cs | 6 +-- IntegrationTests/ITestScope.cs | 2 +- IntegrationTests/IntegrationTests.csproj | 4 +- IntegrationTests/Scenarios.cs | 6 +-- UnitTests/UniqueIdGeneratorTest.cs | 18 ++++---- 12 files changed, 111 insertions(+), 60 deletions(-) create mode 100644 AutoNumber/DataWrapper.cs diff --git a/AutoNumber/BlobOptimisticDataStore.cs b/AutoNumber/BlobOptimisticDataStore.cs index aed2740..6a037dc 100644 --- a/AutoNumber/BlobOptimisticDataStore.cs +++ b/AutoNumber/BlobOptimisticDataStore.cs @@ -14,7 +14,7 @@ namespace AutoNumber { - public class BlobOptimisticDataStore : IOptimisticDataStore + internal class BlobOptimisticDataStore : IOptimisticDataStore { private const string SeedValue = "1"; private readonly BlobContainerClient blobContainer; @@ -32,7 +32,12 @@ public BlobOptimisticDataStore(BlobServiceClient blobServiceClient, IOptions GetDataAsync(string blockName) + public async Task GetDataAsync(string blockName) + { + var result = await GetDataWithConcurrencyCheckAsync(blockName); + + return result.Value; + } + + public async Task GetDataWithConcurrencyCheckAsync(string blockName) { var blobReference = GetBlobReference(blockName); @@ -68,14 +80,19 @@ public bool Init() return result == null || result.Value != null; } - public bool TryOptimisticWrite(string blockName, string data, Azure.ETag? eTag) + 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 = eTag ?? (blobReference.GetProperties()).Value.ETag + IfMatch = eTag }; UploadText( blobReference, @@ -93,14 +110,19 @@ public bool TryOptimisticWrite(string blockName, string data, Azure.ETag? eTag) return true; } - public async Task TryOptimisticWriteAsync(string blockName, string data, Azure.ETag? eTag) + public async Task TryOptimisticWriteAsync(string blockName, string data) + { + return await TryOptimisticWriteWithConcurrencyCheckAsync(blockName, data, Azure.ETag.All); + } + + public async Task TryOptimisticWriteWithConcurrencyCheckAsync(string blockName, string data, Azure.ETag eTag) { var blobReference = GetBlobReference(blockName); try { var blobRequestCondition = new BlobRequestConditions { - IfMatch = eTag ?? (await blobReference.GetPropertiesAsync()).Value.ETag + IfMatch = eTag }; await UploadTextAsync( blobReference, diff --git a/AutoNumber/DataWrapper.cs b/AutoNumber/DataWrapper.cs new file mode 100644 index 0000000..10f1918 --- /dev/null +++ b/AutoNumber/DataWrapper.cs @@ -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; } + } +} \ No newline at end of file diff --git a/AutoNumber/DebugOnlyFileDataStore.cs b/AutoNumber/DebugOnlyFileDataStore.cs index 77e8e9c..184c758 100644 --- a/AutoNumber/DebugOnlyFileDataStore.cs +++ b/AutoNumber/DebugOnlyFileDataStore.cs @@ -1,14 +1,11 @@ using System; using System.IO; -using System.Runtime.CompilerServices; using System.Threading.Tasks; -using System.Xml.Linq; using AutoNumber.Interfaces; -using Azure; namespace AutoNumber { - public class DebugOnlyFileDataStore : IOptimisticDataStore + internal class DebugOnlyFileDataStore : IOptimisticDataStore { private const string SeedValue = "1"; @@ -19,7 +16,12 @@ public DebugOnlyFileDataStore(string directoryPath) this.directoryPath = directoryPath; } - public DataWrapper GetData(string blockName) + public string GetData(string blockName) + { + return GetDataWithConcurrencyCheck(blockName).Value; + } + + public DataWrapper GetDataWithConcurrencyCheck(string blockName) { var blockPath = Path.Combine(directoryPath, $"{blockName}.txt"); try @@ -42,7 +44,12 @@ public DataWrapper GetData(string blockName) } } - public Task GetDataAsync(string blockName) + public Task GetDataAsync(string blockName) + { + throw new NotImplementedException(); + } + + public Task GetDataWithConcurrencyCheckAsync(string blockName) { throw new NotImplementedException(); } @@ -57,25 +64,35 @@ public bool Init() return true; } - public bool TryOptimisticWrite(string blockName, string data, Azure.ETag? eTag) + 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 (eTag.HasValue) - { - if(!File.Exists(blockPath)) return false; + 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.Value)) return false; + if (!eTagToCompare.Equals(eTag)) return false; } File.WriteAllText(blockPath, data); return true; } - public Task TryOptimisticWriteAsync(string blockName, string data, Azure.ETag? eTag) + public Task TryOptimisticWriteAsync(string blockName, string data) + { + throw new NotImplementedException(); + } + + public Task TryOptimisticWriteWithConcurrencyCheckAsync(string blockName, string data, Azure.ETag eTag) { throw new NotImplementedException(); } diff --git a/AutoNumber/Interfaces/IOptimisticDataStore.cs b/AutoNumber/Interfaces/IOptimisticDataStore.cs index ea8cc7c..57611e7 100644 --- a/AutoNumber/Interfaces/IOptimisticDataStore.cs +++ b/AutoNumber/Interfaces/IOptimisticDataStore.cs @@ -6,24 +6,19 @@ namespace AutoNumber.Interfaces { public interface IOptimisticDataStore { - DataWrapper GetData(string blockName); - Task GetDataAsync(string blockName); - bool TryOptimisticWrite(string blockName, string data, Azure.ETag? eTag); - Task TryOptimisticWriteAsync(string blockName, string data, Azure.ETag? eTag); - Task InitAsync(); - bool Init(); - } + string GetData(string blockName); + DataWrapper GetDataWithConcurrencyCheck(string blockName); - public class DataWrapper - { - public DataWrapper(string value, Azure.ETag eTag) - { - Value = value; - ETag = eTag; - } + Task GetDataAsync(string blockName); + Task GetDataWithConcurrencyCheckAsync(string blockName); - public Azure.ETag ETag { get; private set; } + bool TryOptimisticWrite(string blockName, string data); + bool TryOptimisticWriteWithConcurrencyCheck(string blockName, string data, Azure.ETag eTag); - public string Value { get; private set; } + Task TryOptimisticWriteAsync(string blockName, string data); + + Task TryOptimisticWriteWithConcurrencyCheckAsync(string blockName, string data, Azure.ETag eTag); + Task InitAsync(); + bool Init(); } } \ No newline at end of file diff --git a/AutoNumber/InternalsVisibleTo.cs b/AutoNumber/InternalsVisibleTo.cs index ab2f090..a50c564 100644 --- a/AutoNumber/InternalsVisibleTo.cs +++ b/AutoNumber/InternalsVisibleTo.cs @@ -2,4 +2,6 @@ using System.IO; using System.Runtime.CompilerServices; -[assembly: InternalsVisibleTo("AutoNumber.UnitTests")] \ No newline at end of file +[assembly: InternalsVisibleTo("AutoNumber.UnitTests")] +[assembly: InternalsVisibleTo("AutoNumber.IntegrationTests")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] \ No newline at end of file diff --git a/AutoNumber/UniqueIdGenerator.cs b/AutoNumber/UniqueIdGenerator.cs index f21f00e..dc2d65b 100644 --- a/AutoNumber/UniqueIdGenerator.cs +++ b/AutoNumber/UniqueIdGenerator.cs @@ -13,7 +13,7 @@ namespace AutoNumber /// /// Generate a new incremental id regards the scope name /// - public class UniqueIdGenerator : IUniqueIdGenerator + internal class UniqueIdGenerator : IUniqueIdGenerator { /// /// Generate a new incremental id regards the scope name @@ -47,7 +47,7 @@ private void UpdateFromSyncStore(string scopeName, ScopeState state) while (writesAttempted < MaxWriteAttempts) { - var data = optimisticDataStore.GetData(scopeName); + var data = optimisticDataStore.GetDataWithConcurrencyCheck(scopeName); if (!long.TryParse(data.Value, out var nextId)) throw new UniqueIdGenerationException( @@ -57,7 +57,7 @@ private void UpdateFromSyncStore(string scopeName, ScopeState state) state.HighestIdAvailableInBatch = nextId - 1 + BatchSize; var firstIdInNextBatch = state.HighestIdAvailableInBatch + 1; - if (optimisticDataStore.TryOptimisticWrite(scopeName, + if (optimisticDataStore.TryOptimisticWriteWithConcurrencyCheck(scopeName, firstIdInNextBatch.ToString(CultureInfo.InvariantCulture), data.ETag)) return; diff --git a/IntegrationTests/Azure.cs b/IntegrationTests/Azure.cs index 03d0b3d..1f7813d 100644 --- a/IntegrationTests/Azure.cs +++ b/IntegrationTests/Azure.cs @@ -7,19 +7,19 @@ using Azure.Storage.Blobs.Specialized; using NUnit.Framework; -namespace IntegrationTests.cs +namespace AutoNumber.IntegrationTests { [TestFixture] public class Azure : Scenarios { 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(); diff --git a/IntegrationTests/File.cs b/IntegrationTests/File.cs index 27aceae..395b8b6 100644 --- a/IntegrationTests/File.cs +++ b/IntegrationTests/File.cs @@ -4,17 +4,17 @@ using AutoNumber.Interfaces; using NUnit.Framework; -namespace IntegrationTests.cs +namespace AutoNumber.IntegrationTests { [TestFixture] public class File : Scenarios { - 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); } diff --git a/IntegrationTests/ITestScope.cs b/IntegrationTests/ITestScope.cs index 4b64b30..a395ee2 100644 --- a/IntegrationTests/ITestScope.cs +++ b/IntegrationTests/ITestScope.cs @@ -1,6 +1,6 @@ using System; -namespace IntegrationTests.cs +namespace AutoNumber.IntegrationTests { public interface ITestScope : IDisposable { diff --git a/IntegrationTests/IntegrationTests.csproj b/IntegrationTests/IntegrationTests.csproj index 4187655..2db6462 100644 --- a/IntegrationTests/IntegrationTests.csproj +++ b/IntegrationTests/IntegrationTests.csproj @@ -1,8 +1,8 @@  8.0.30703 - IntegrationTests.cs - IntegrationTests.cs + AutoNumber.IntegrationTests + AutoNumber.IntegrationTests ..\ IntegrationTests.cs Microsoft diff --git a/IntegrationTests/Scenarios.cs b/IntegrationTests/Scenarios.cs index f63f8c0..d681402 100644 --- a/IntegrationTests/Scenarios.cs +++ b/IntegrationTests/Scenarios.cs @@ -6,12 +6,12 @@ using AutoNumber.Interfaces; using NUnit.Framework; -namespace IntegrationTests.cs +namespace AutoNumber.IntegrationTests { public abstract class Scenarios 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() diff --git a/UnitTests/UniqueIdGeneratorTest.cs b/UnitTests/UniqueIdGeneratorTest.cs index 7f3d33a..ab1843b 100644 --- a/UnitTests/UniqueIdGeneratorTest.cs +++ b/UnitTests/UniqueIdGeneratorTest.cs @@ -51,11 +51,11 @@ public void NextIdShouldReturnNumbersSequentially() { var eTagDate = new DateTime(2000, 1, 2, 3, 4, 5, 6, DateTimeKind.Utc); var store = Substitute.For(); - store.GetData("test") + store.GetDataWithConcurrencyCheck("test") .Returns( new DataWrapper("0", ETag.ForDate(eTagDate)), new DataWrapper("250", ETag.ForDate(eTagDate.AddMonths(1)))); - store.TryOptimisticWrite("test", "3", ETag.ForDate(eTagDate)) + store.TryOptimisticWriteWithConcurrencyCheck("test", "3", ETag.ForDate(eTagDate)) .Returns(true); var subject = new UniqueIdGenerator(store) @@ -74,11 +74,11 @@ public void NextIdShouldRollOverToNewBlockWhenCurrentBlockIsExhausted() var eTagDate = new DateTime(2000, 1, 2, 3, 4, 5, 6, DateTimeKind.Utc); var eTagDate2 = eTagDate.AddMonths(1); var store = Substitute.For(); - store.GetData("test").Returns( + store.GetDataWithConcurrencyCheck("test").Returns( new DataWrapper("0", ETag.ForDate(eTagDate)), new DataWrapper("250", ETag.ForDate(eTagDate2))); - store.TryOptimisticWrite("test", "3", ETag.ForDate(eTagDate)).Returns(true); - store.TryOptimisticWrite("test", "253", ETag.ForDate(eTagDate2)).Returns(true); + store.TryOptimisticWriteWithConcurrencyCheck("test", "3", ETag.ForDate(eTagDate)).Returns(true); + store.TryOptimisticWriteWithConcurrencyCheck("test", "253", ETag.ForDate(eTagDate2)).Returns(true); var subject = new UniqueIdGenerator(store) { @@ -97,7 +97,7 @@ public void NextIdShouldRollOverToNewBlockWhenCurrentBlockIsExhausted() public void NextIdShouldThrowExceptionOnCorruptData() { var store = Substitute.For(); - store.GetData("test").Returns(new DataWrapper("abc", Azure.ETag.All)); + store.GetDataWithConcurrencyCheck("test").Returns(new DataWrapper("abc", Azure.ETag.All)); Assert.That(() => { @@ -111,7 +111,7 @@ public void NextIdShouldThrowExceptionOnCorruptData() public void NextIdShouldThrowExceptionOnNullData() { var store = Substitute.For(); - store.GetData("test").Returns(new DataWrapper((string)null, Azure.ETag.All)); + store.GetDataWithConcurrencyCheck("test").Returns(new DataWrapper((string)null, Azure.ETag.All)); Assert.That(() => { @@ -126,8 +126,8 @@ public void NextIdShouldThrowExceptionWhenRetriesAreExhausted() { var eTagDate = new DateTime(2000, 1, 2, 3, 4, 5, 6, DateTimeKind.Utc); var store = Substitute.For(); - store.GetData("test").Returns(new DataWrapper("0", ETag.ForDate(eTagDate))); - store.TryOptimisticWrite("test", "3", ETag.ForDate(eTagDate.AddMonths(1))) + store.GetDataWithConcurrencyCheck("test").Returns(new DataWrapper("0", ETag.ForDate(eTagDate))); + store.TryOptimisticWriteWithConcurrencyCheck("test", "3", ETag.ForDate(eTagDate.AddMonths(1))) .Returns(false, false, false, true); var generator = new UniqueIdGenerator(store)