diff --git a/Bantam/Bantam.csproj b/Bantam/Bantam.csproj index 743d409..7f7f4d2 100644 --- a/Bantam/Bantam.csproj +++ b/Bantam/Bantam.csproj @@ -1,4 +1,4 @@ - + Debug @@ -31,6 +31,7 @@ + diff --git a/Bantam/CommandChainExecutor.cs b/Bantam/CommandChainExecutor.cs index bd6a049..a5456b5 100644 --- a/Bantam/CommandChainExecutor.cs +++ b/Bantam/CommandChainExecutor.cs @@ -73,6 +73,7 @@ internal void Start(Event triggeringEvent, CommandChain chain, CommandRelay mana this.pool = pool; failureAllocator = chain.FailureCommand; enumerator = chain.Commands.GetEnumerator(); + pool.Lock(triggeringEvent, this); enumerator.MoveNext(); Next(); } @@ -84,7 +85,10 @@ public void CurrentCommandComplete() if (enumerator.MoveNext()) Next(); else + { + pool.Unlock(triggeringEvent.GetType(), triggeringEvent, this); manager.CompleteChainExecution(this); + } } public void CurrentCommandFailed() diff --git a/Bantam/MultiLock.cs b/Bantam/MultiLock.cs new file mode 100644 index 0000000..e815ea7 --- /dev/null +++ b/Bantam/MultiLock.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; + +namespace Bantam +{ + public class MultiLock : Poolable + { + public bool IsLocked { get { return keys.Count > 0; } } + private readonly Dictionary keys = new Dictionary(); + + public void Lock(object key) + { + keys[key] = true; + } + + public void Unlock(object key) + { + keys.Remove(key); + } + + public void Reset() + { + keys.Clear(); + } + } +} diff --git a/Bantam/ObjectPool.cs b/Bantam/ObjectPool.cs index b10b174..b4dda38 100644 --- a/Bantam/ObjectPool.cs +++ b/Bantam/ObjectPool.cs @@ -1,44 +1,78 @@ using System; -using System.Collections; using System.Collections.Generic; namespace Bantam { public class ObjectPool { - private Dictionary> instances = new Dictionary>(); public Dictionary UniqueInstances = new Dictionary(); + private readonly Dictionary> instances = new Dictionary>(); + private readonly Dictionary lockedInstances = new Dictionary(); + public T Allocate() where T : class, Poolable, new() { EnsurePoolExists(); - return GetInstance(); + var instance = GetInstance(); + Lock(instance, this); + return instance; } public Poolable Allocate(Type type) { ValidateType(type); EnsurePoolExists(type); - return GetInstance(type); + var instance = GetInstance(type); + Lock(instance, this); + return instance; } public void Free(T instance) where T : Poolable { if (null == instance) throw new NullInstanceException(); - EnsurePoolExists(); - instances[typeof(T)].Enqueue(instance); + Unlock(instance, this); } public void Free(Type type, Poolable instance) + { + Unlock(type, instance, this); + } + + public void Lock(Poolable instance, object key) + { + EnsureLockExists(instance); + lockedInstances[instance].Lock(key); + } + + public void Unlock(T instance, object key) where T : Poolable + { + Unlock(typeof(T), instance, key); + } + + public void Unlock(Type type, Poolable instance, object key) + { + Validate(type, instance); + MultiLock instanceLock; + lockedInstances.TryGetValue(instance, out instanceLock); + if (null == instanceLock) + return; + instanceLock.Unlock(key); + if (!instanceLock.IsLocked) + { + lockedInstances.Remove(instance); + FreeInternalLock(instanceLock); + InternalFree(type, instance); + } + } + + private void Validate(Type type, Poolable instance) { if (null == instance) throw new NullInstanceException(); ValidateType(type); if (!type.IsInstanceOfType(instance)) throw new MismatchedTypeException(); - EnsurePoolExists(type); - instances[type].Enqueue(instance); } private void EnsurePoolExists() @@ -55,6 +89,12 @@ private void EnsurePoolExists(Type type) } } + private void EnsureLockExists(Poolable instance) + { + if (!lockedInstances.ContainsKey(instance)) + lockedInstances[instance] = AllocateInternalLock(); + } + private void ValidateType(Type type) { if (!type.IsClass) @@ -93,6 +133,24 @@ private Poolable GetInstance(Type type) instance.Reset(); return instance; } + + private MultiLock AllocateInternalLock() + { + EnsurePoolExists(); + return GetInstance(); + } + + private void FreeInternalLock(MultiLock internalLock) + { + EnsurePoolExists(); + instances[typeof(MultiLock)].Enqueue(internalLock); + } + + private void InternalFree(Type type, Poolable instance) + { + EnsurePoolExists(type); + instances[type].Enqueue(instance); + } } public class ObjectPoolException : Exception {} diff --git a/BantamTest/BantamTest.csproj b/BantamTest/BantamTest.csproj index 1156e3e..335287b 100644 --- a/BantamTest/BantamTest.csproj +++ b/BantamTest/BantamTest.csproj @@ -1,4 +1,4 @@ - + Debug @@ -28,15 +28,13 @@ false - - - ..\packages\NUnit.2.6.4\lib\nunit.framework.dll + + ..\packages\NUnit.3.6.1\lib\net45\nunit.framework.dll + True + - - - {DD312BD0-4A72-4F52-8969-05313D2AEF35} @@ -44,10 +42,17 @@ + + + + + + + \ No newline at end of file diff --git a/BantamTest/CommandRelayTest.cs b/BantamTest/CommandRelayTest.cs index f93dd18..3b83ecf 100644 --- a/BantamTest/CommandRelayTest.cs +++ b/BantamTest/CommandRelayTest.cs @@ -1,4 +1,5 @@ -using NUnit.Framework; +using System.Collections.Generic; +using NUnit.Framework; namespace Bantam.Test { @@ -16,6 +17,8 @@ public void SetUp() testObj = new CommandRelay(eventBus, pool); DummyCommand.ExecuteCount = 0; DummyCommand.LastValue = 0; + AsyncCommand.triggeringEvents.Clear(); + AsyncCommand.ClearAll(); } [Test] @@ -103,6 +106,26 @@ public void OnFailureCanTakeAnOptionalInitializerForCommand() eventBus.Dispatch(evt => evt.value = expectedValue); Assert.AreEqual(expectedValue, DummyCommand.LastValue); } + + [Test] + public void AsyncCommandChainsLockTriggeringEvent() + { + testObj.On().Do((cmd, evt) => cmd.trigger = evt); + eventBus.Dispatch(); + eventBus.Dispatch(); + Assert.AreNotSame(AsyncCommand.triggeringEvents[0], AsyncCommand.triggeringEvents[1]); + } + + [Test] + public void AsyncCommandChainsReleaseLockOnTriggeringEventWhenTheyComplete() + { + testObj.On().Do((cmd, evt) => cmd.trigger = evt); + eventBus.Dispatch(); + eventBus.Dispatch(); + AsyncCommand.CompleteAll(); + eventBus.Dispatch(); + Assert.AreSame(AsyncCommand.triggeringEvents[0], AsyncCommand.triggeringEvents[2]); + } } public class DummyCommand : Command @@ -126,4 +149,36 @@ public override void Execute() Fail(); } } + + public class AsyncCommand : Command + { + private static List commands = new List(); + public static List triggeringEvents = new List(); + public Event trigger; + + public override void Execute() + { + commands.Add(this); + Retain(); + triggeringEvents.Add(trigger); + } + + public static void CompleteAll() + { + foreach (var c in commands) + c.Done(); + ClearAll(); + } + + public static void ClearAll() + { + commands.Clear(); + } + + public override void Reset() + { + base.Reset(); + trigger = null; + } + } } diff --git a/BantamTest/IntegrationTest.cs b/BantamTest/IntegrationTest.cs index 604297c..2fa4130 100644 --- a/BantamTest/IntegrationTest.cs +++ b/BantamTest/IntegrationTest.cs @@ -62,7 +62,7 @@ public void FullIntegrationTest() Assert.AreEqual(1, pool.UniqueInstances[typeof(LoginEvent)]); Assert.AreEqual(1, pool.UniqueInstances[typeof(LoginFailedEvent)]); - Assert.AreEqual(1, pool.UniqueInstances[typeof(LoginSuccessEvent)]); + Assert.AreEqual(2, pool.UniqueInstances[typeof(LoginSuccessEvent)]); Assert.AreEqual(2, pool.UniqueInstances[typeof(LoginCommand)]); //A second LoginCommand is needed to respond to the first one failing before the first LoginCommand's Done method is called. Assert.AreEqual(1, pool.UniqueInstances[typeof(RecordLoginCommand)]); Assert.AreEqual(1, pool.UniqueInstances[typeof(UpdateDisplayNameCommand)]); diff --git a/BantamTest/MultiLockTest.cs b/BantamTest/MultiLockTest.cs new file mode 100644 index 0000000..488b4e5 --- /dev/null +++ b/BantamTest/MultiLockTest.cs @@ -0,0 +1,85 @@ +using NUnit.Framework; + +namespace Bantam.Test +{ + [TestFixture] + public class MultiLockTest + { + private MultiLock testObj; + private object key, secondKey; + + [SetUp] + public void SetUp() + { + testObj = new MultiLock(); + key = new object(); + secondKey = new object(); + } + + [Test] + public void IsLockedIsFalseByDefault() + { + Assert.IsFalse(testObj.IsLocked); + } + + [Test] + public void IsLockedIsTrueAfterLockIsCalled() + { + testObj.Lock(key); + Assert.IsTrue(testObj.IsLocked); + } + + [Test] + public void IsLockedIsFalseAfterUnlockIsCalledWithKey() + { + testObj.Lock(key); + testObj.Unlock(key); + Assert.IsFalse(testObj.IsLocked); + } + + [Test] + public void IsLockedIsTrueAfterUnlockIsCalledWithIncorrectKey() + { + testObj.Lock(key); + testObj.Unlock(secondKey); + Assert.IsTrue(testObj.IsLocked); + } + + [Test] + public void IsLockedIsTrueIfLockedWithTwoKeysAndOnlyUnlockedWithFirstKey() + { + testObj.Lock(key); + testObj.Lock(secondKey); + testObj.Unlock(key); + Assert.IsTrue(testObj.IsLocked); + } + + [Test] + public void IsLockedIsTrueIfLockedWithTwoKeysAndOnlyUnlockedWithSecondKey() + { + testObj.Lock(key); + testObj.Lock(secondKey); + testObj.Unlock(secondKey); + Assert.IsTrue(testObj.IsLocked); + } + + [Test] + public void IsLockedIsFalseIfLockWithTwoKeysAndUnlockedWithBothKeys() + { + testObj.Lock(key); + testObj.Lock(secondKey); + testObj.Unlock(key); + testObj.Unlock(secondKey); + Assert.IsFalse(testObj.IsLocked); + } + + [Test] + public void ResetClearsAllKeys() + { + testObj.Lock(key); + testObj.Lock(secondKey); + testObj.Reset(); + Assert.IsFalse(testObj.IsLocked); + } + } +} diff --git a/BantamTest/ObjectPoolTest.cs b/BantamTest/ObjectPoolTest.cs index 41ffa4a..2d06572 100644 --- a/BantamTest/ObjectPoolTest.cs +++ b/BantamTest/ObjectPoolTest.cs @@ -1,5 +1,4 @@ -using System; -using NUnit.Framework; +using NUnit.Framework; namespace Bantam.Test { @@ -40,7 +39,7 @@ public void AllocateReturnsDifferentObjectsOnSubsequentCalls() public void FreeAllowsObjectToBeUsedAgain() { var first = testObj.Allocate(); - testObj.Free(first); + testObj.Free(first); var second = testObj.Allocate(); Assert.AreSame(first, second); } @@ -110,6 +109,163 @@ public void FreeWithTypeThrowsExceptionIfInstanceDoesNotMatchGivenType() Assert.Throws(() => testObj.Free(typeof(DummyType), new DummyEvent())); } + [Test] + public void LockOnAnInstancePreventsItFromBeingAllocated() + { + var key = new object(); + var first = testObj.Allocate(); + testObj.Lock(first, key); + testObj.Free(first); + var second = testObj.Allocate(); + Assert.AreNotSame(first, second); + } + + [Test] + public void LockAndUnlockWithSameKeyOnAnInstanceAllowsItToBeAllocated() + { + var key = new object(); + var first = testObj.Allocate(); + testObj.Lock(first, key); + testObj.Free(first); + testObj.Unlock(first, key); + var second = testObj.Allocate(); + Assert.AreSame(first, second); + } + + [Test] + public void LockAndUnlockWithDifferentKeysOnAnInstanceDoesNotAllowItToBeAllocated() + { + var firstKey = new object(); + var secondKey = new object(); + var first = testObj.Allocate(); + testObj.Lock(first, firstKey); + testObj.Free(first); + testObj.Unlock(first, secondKey); + var second = testObj.Allocate(); + Assert.AreNotSame(first, second); + } + + [Test] + public void UnlockingInstanceWithoutLockingItDoesNotAddItToPool() + { + var key = new object(); + var first = new DummyType(); + testObj.Unlock(first, key); + var second = testObj.Allocate(); + Assert.AreNotSame(first, second); + } + + [Test] + public void LockOnAnInstancePreventsItFromBeingAllocatedWithType() + { + var key = new object(); + var first = testObj.Allocate(typeof(DummyType)); + testObj.Lock(first, key); + testObj.Free(typeof(DummyType), first); + var second = testObj.Allocate(typeof(DummyType)); + Assert.AreNotSame(first, second); + } + + [Test] + public void LockAndUnlockWithTypeAndSameKeyOnAnInstanceAllowsItToBeAllocated() + { + var key = new object(); + var first = testObj.Allocate(typeof(DummyType)); + testObj.Lock(first, key); + testObj.Free(typeof(DummyType), first); + testObj.Unlock(typeof(DummyType), first, key); + var second = testObj.Allocate(typeof(DummyType)); + Assert.AreSame(first, second); + } + + [Test] + public void LockAndUnlockWithTypeAndDifferentKeysOnAnInstanceDoesNotAllowItToBeAllocated() + { + var firstKey = new object(); + var secondKey = new object(); + var first = testObj.Allocate(typeof(DummyType)); + testObj.Lock(first, firstKey); + testObj.Free(typeof(DummyType), first); + testObj.Unlock(typeof(DummyType), first, secondKey); + var second = testObj.Allocate(typeof(DummyType)); + Assert.AreNotSame(first, second); + } + + [Test] + public void UnlockingInstanceWithTypeWithoutLockingItDoesNotAddItToPool() + { + var key = new object(); + var first = new DummyType(); + testObj.Unlock(typeof(DummyType), first, key); + var second = testObj.Allocate(typeof(DummyType)); + Assert.AreNotSame(first, second); + } + + [Test] + public void LockingAndUnlockingAllocatedInstanceWithoutFreeingItDoesNotAllowItToBeAllocatedAgain() + { + var key = new object(); + var first = testObj.Allocate(); + testObj.Lock(first, key); + testObj.Unlock(first, key); + var second = testObj.Allocate(); + Assert.AreNotSame(first, second); + } + + [Test] + public void LockingAndUnlockingAllocatedWithTypeInstanceWithoutFreeingItDoesNotAllowItToBeAllocatedAgain() + { + var key = new object(); + var first = testObj.Allocate(typeof(DummyType)) as DummyType; + testObj.Lock(first, key); + testObj.Unlock(first, key); + var second = testObj.Allocate(typeof(DummyType)) as DummyType; + Assert.AreNotSame(first, second); + } + + [Test] + public void AllocatingAndFreeingInstanceDoesNotAddItToPoolTwice() + { + var first = testObj.Allocate(); + testObj.Free(first); + var second = testObj.Allocate(); + var third = testObj.Allocate(); + Assert.AreNotSame(second, third); + } + + [Test] + public void AllocatingAndFreeingWithTypeInstanceDoesNotAddItToPoolTwice() + { + var first = testObj.Allocate(typeof(DummyType)) as DummyType; + testObj.Free(typeof(DummyType), first); + var second = testObj.Allocate(typeof(DummyType)) as DummyType; + var third = testObj.Allocate(typeof(DummyType)) as DummyType; + Assert.AreNotSame(second, third); + } + + [Test] + public void UnlockWithTypeThrowsExceptionForInvalidTypes() + { + var key = new object(); + Assert.Throws(() => testObj.Unlock(typeof(NonPoolableType), new DummyType(), key)); + Assert.Throws(() => testObj.Unlock(typeof(PoolableStruct), new PoolableStruct(), key)); + Assert.Throws(() => testObj.Unlock(typeof(PoolableWithConstructor), new PoolableWithConstructor(5), key)); + } + + [Test] + public void UnlockWithTypeThrowsExceptionIfInstanceIsNull() + { + var key = new object(); + Assert.Throws(() => testObj.Unlock(typeof(DummyType), null, key)); + } + + [Test] + public void UnlockWithTypeThrowsExceptionIfInstanceDoesNotMatchGivenType() + { + var key = new object(); + Assert.Throws(() => testObj.Unlock(typeof(DummyType), new DummyEvent(), key)); + } + [Test] public void UniqueInstancesReflectsTheNumberOfAllocationsPoolHasDoneForEachType() { diff --git a/BantamTest/packages.config b/BantamTest/packages.config index c714ef3..8e3be78 100644 --- a/BantamTest/packages.config +++ b/BantamTest/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file