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()));
}
///