diff --git a/Sources/Linq2DynamoDb.AspNet.DataSource/UpdatableDataContext.cs b/Sources/Linq2DynamoDb.AspNet.DataSource/UpdatableDataContext.cs index 6cc04c9..15dc177 100644 --- a/Sources/Linq2DynamoDb.AspNet.DataSource/UpdatableDataContext.cs +++ b/Sources/Linq2DynamoDb.AspNet.DataSource/UpdatableDataContext.cs @@ -147,7 +147,7 @@ void IUpdatable.DeleteResource(object targetResource) // The only thing we can do is to try to remove the entity from all of them. foreach (var pair in this.TableWrappers.Where(pair => pair.Key.Item1 == entityType)) { - pair.Value.RemoveEntity(targetResource); + pair.Value.Value.RemoveEntity(targetResource); } } @@ -165,7 +165,7 @@ void IUpdatable.ClearChanges() { foreach (var pair in this.TableWrappers) { - pair.Value.ClearModifications(); + pair.Value.Value.ClearModifications(); } } diff --git a/Sources/Linq2DynamoDb.DataContext.Tests/DataContextTests.cs b/Sources/Linq2DynamoDb.DataContext.Tests/DataContextTests.cs index e916b43..9be4054 100644 --- a/Sources/Linq2DynamoDb.DataContext.Tests/DataContextTests.cs +++ b/Sources/Linq2DynamoDb.DataContext.Tests/DataContextTests.cs @@ -1,11 +1,13 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using System.Text; +using System.Threading; using System.Threading.Tasks; - +using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.Model; using Linq2DynamoDb.DataContext.Tests.Entities; - +using NSubstitute; using NUnit.Framework; namespace Linq2DynamoDb.DataContext.Tests @@ -51,5 +53,103 @@ public void DataContext() var result = table.FirstOrDefault(); Assert.IsNotNull(result); } + + /// Set to run 10 (magic number) times, as race conditions are hard to get to consistently fail. + [Test, Repeat(10)] + public async Task GetTableDefinitionRaceConditionRunningSomewhatInParallelSynchronised() + { + //setup fake dynamodb interactions + var dynamoDbClient = Substitute.For(); + dynamoDbClient.DescribeTable(Arg.Any()).Returns(new DescribeTableResponse + { + Table = new TableDescription { TableStatus = TableStatus.ACTIVE, KeySchema = new List { new KeySchemaElement("key", KeyType.HASH) }, AttributeDefinitions = new List { new AttributeDefinition("key", ScalarAttributeType.S) } } + }); + + //subject + var context = new DataContext(dynamoDbClient); + context.CreateTableIfNotExists(new CreateTableArgs(c => c.Name)); + + const int numberOfRacers = 10; + + //exercise + var checkeredFlag = new SemaphoreSlim(0); + var racers = Enumerable.Range(0, numberOfRacers).Select(i => Task.Run(async () => + { + await checkeredFlag.WaitAsync(); + return context.GetTable(); + })); + checkeredFlag.Release(numberOfRacers); + var tables = await Task.WhenAll(racers); + var actual = tables.OfType().Select(t => t.TableWrapper).ToList(); + + //assert + var expected = Enumerable.Repeat(actual.First(), numberOfRacers).ToList(); + CollectionAssert.AreEqual(expected, actual); + } + + /// Set to run 10 (magic number) times, as race conditions are hard to get to consistently fail. + [Test, Repeat(10)] + public async Task GetTableDefinitionRaceConditionRunningAsCloseToParallelSynchronised() + { + //setup fake dynamodb interactions + var dynamoDbClient = Substitute.For(); + dynamoDbClient.DescribeTable(Arg.Any()).Returns(new DescribeTableResponse + { + Table = new TableDescription { TableStatus = TableStatus.ACTIVE, KeySchema = new List { new KeySchemaElement("key", KeyType.HASH) }, AttributeDefinitions = new List { new AttributeDefinition("key", ScalarAttributeType.S) } } + }); + + //subject + var context = new DataContext(dynamoDbClient); + context.CreateTableIfNotExists(new CreateTableArgs(c => c.Name)); + + const int numberOfRacers = 10; + + //exercise + var flagman = new AsyncBarrier(numberOfRacers); + var racers = Enumerable.Range(0, numberOfRacers).Select(i => Task.Run(async () => + { + await flagman.SignalAndWait(); + return context.GetTable(); + })); + + var tables = await Task.WhenAll(racers); + var actual = tables.OfType().Select(t => t.TableWrapper).ToList(); + + //assert + var expected = Enumerable.Repeat(actual.First(), numberOfRacers).ToList(); + CollectionAssert.AreEqual(expected, actual); + } + } + + /// + /// Thanks to Stephen Toub :) + /// http://blogs.msdn.com/b/pfxteam/archive/2012/02/11/10266932.aspx + /// + public class AsyncBarrier + { + private readonly int _participantCount; + private int _remainingParticipants; + private ConcurrentStack> m_waiters; + + public AsyncBarrier(int participantCount) + { + if (participantCount <= 0) throw new ArgumentOutOfRangeException(nameof(participantCount)); + _remainingParticipants = _participantCount = participantCount; + m_waiters = new ConcurrentStack>(); + } + + public Task SignalAndWait() + { + var tcs = new TaskCompletionSource(); + m_waiters.Push(tcs); + if (Interlocked.Decrement(ref _remainingParticipants) == 0) + { + _remainingParticipants = _participantCount; + var waiters = m_waiters; + m_waiters = new ConcurrentStack>(); + Parallel.ForEach(waiters, w => w.SetResult(true)); + } + return tcs.Task; + } } } diff --git a/Sources/Linq2DynamoDb.DataContext.Tests/Linq2DynamoDb.DataContext.Tests.csproj b/Sources/Linq2DynamoDb.DataContext.Tests/Linq2DynamoDb.DataContext.Tests.csproj index 409b9aa..c62b6f7 100644 --- a/Sources/Linq2DynamoDb.DataContext.Tests/Linq2DynamoDb.DataContext.Tests.csproj +++ b/Sources/Linq2DynamoDb.DataContext.Tests/Linq2DynamoDb.DataContext.Tests.csproj @@ -51,6 +51,10 @@ False ..\packages\log4net.2.0.2\lib\net40-full\log4net.dll + + ..\packages\NSubstitute.1.10.0.0\lib\net45\NSubstitute.dll + True + ..\packages\NUnit.2.6.3\lib\nunit.framework.dll diff --git a/Sources/Linq2DynamoDb.DataContext.Tests/packages.config b/Sources/Linq2DynamoDb.DataContext.Tests/packages.config index f925803..1e0fd83 100644 --- a/Sources/Linq2DynamoDb.DataContext.Tests/packages.config +++ b/Sources/Linq2DynamoDb.DataContext.Tests/packages.config @@ -4,6 +4,7 @@ + \ No newline at end of file diff --git a/Sources/Linq2DynamoDb.DataContext/DataContext.cs b/Sources/Linq2DynamoDb.DataContext/DataContext.cs index 43cca64..9e2f69d 100644 --- a/Sources/Linq2DynamoDb.DataContext/DataContext.cs +++ b/Sources/Linq2DynamoDb.DataContext/DataContext.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Amazon; @@ -124,7 +125,7 @@ public DataTable GetTable(object hashKeyValue, Func(entityType, hashKeyValue), - t => + t => new Lazy(() => { // if cache is not provided, then passing a fake implementation var cacheImplementation = @@ -144,8 +145,8 @@ public DataTable GetTable(object hashKeyValue, Func GetTable(object hashKeyValue, Func public void SubmitChanges() { - Task.WaitAll(this.TableWrappers.Values.Select(t => t.SubmitChangesAsync()).ToArray()); + Task.WaitAll(this.TableWrappers.Values.Select(t => t.Value.SubmitChangesAsync()).ToArray()); } #endregion @@ -193,7 +194,7 @@ public void SubmitChanges() /// /// TableDefinitionWrapper instances for each entity type and HashKey value (if specified) /// - protected internal readonly ConcurrentDictionary, TableDefinitionWrapper> TableWrappers = new ConcurrentDictionary, TableDefinitionWrapper>(); + protected internal readonly ConcurrentDictionary, Lazy> TableWrappers = new ConcurrentDictionary, Lazy>(); /// /// A fake cache implementation, which does no caching @@ -201,7 +202,7 @@ public void SubmitChanges() private static readonly ITableCache FakeCacheImplementation = new FakeTableCache(); - private class CachedTableDefinitions : ConcurrentDictionary + private class CachedTableDefinitions : ConcurrentDictionary> { /// /// Instead of storing a reference to DynamoDBClient we're storing it's HashCode @@ -219,6 +220,11 @@ public bool IsAssignedToThisClientInstance(IAmazonDynamoDB client) } } + /// + /// Used to lock while updating the field + /// + private static readonly object CacheTableDefinitionsLock = new object(); + /// /// Table objects are cached here per DynamoDBClient instance. /// In order not to reload table metadata between DataContext instance creations. @@ -269,20 +275,28 @@ private Table GetTableDefinition(Type entityType) throw new InvalidOperationException("An instance of AmazonDynamoDbClient was not provided. Use either a ctor, that takes AmazonDynamoDbClient instance or GetTable() method, that takes Table object"); } - var cachedTableDefinitions = _cachedTableDefinitions; if ( - (cachedTableDefinitions == null) + (_cachedTableDefinitions == null) || - (!cachedTableDefinitions.IsAssignedToThisClientInstance(this._client)) + (!_cachedTableDefinitions.IsAssignedToThisClientInstance(this._client)) ) { - cachedTableDefinitions = new CachedTableDefinitions(this._client); - _cachedTableDefinitions = cachedTableDefinitions; + lock (CacheTableDefinitionsLock) + { + if ((_cachedTableDefinitions == null) + || + (!_cachedTableDefinitions.IsAssignedToThisClientInstance(this._client))) + { + _cachedTableDefinitions = new CachedTableDefinitions(this._client); + } + } } + var cachedTableDefinitions = _cachedTableDefinitions; + string tableName = this.GetTableNameForType(entityType); - return cachedTableDefinitions.GetOrAdd(tableName, name => Table.LoadTable(this._client, name)); + return cachedTableDefinitions.GetOrAdd(tableName, name => new Lazy(() => Table.LoadTable(this._client, name), LazyThreadSafetyMode.ExecutionAndPublication)).Value; } #endregion diff --git a/Sources/Linq2DynamoDb.DataContext/DataContextExtensions.cs b/Sources/Linq2DynamoDb.DataContext/DataContextExtensions.cs index 50d9f02..e3e2e9d 100644 --- a/Sources/Linq2DynamoDb.DataContext/DataContextExtensions.cs +++ b/Sources/Linq2DynamoDb.DataContext/DataContextExtensions.cs @@ -23,7 +23,7 @@ public static class DataContextExtensions /// public static async Task SubmitChangesAsync(this DataContext context) { - await Task.WhenAll(context.TableWrappers.Values.Select(t => t.SubmitChangesAsync())); + await Task.WhenAll(context.TableWrappers.Values.Select(t => t.Value.SubmitChangesAsync())); } ///