diff --git a/src/Pool.Tests/Fakes/EchoMapFactory.cs b/src/Pool.Tests/Fakes/EchoMapFactory.cs new file mode 100644 index 0000000..b586c96 --- /dev/null +++ b/src/Pool.Tests/Fakes/EchoMapFactory.cs @@ -0,0 +1,20 @@ + +namespace Pool.Tests.Fakes; + +internal sealed class EchoMapFactory + : IItemFactory + , IPreparationStrategy +{ + [System.Diagnostics.CodeAnalysis.SuppressMessage("IDisposableAnalyzers.Correctness", "IDISP005:Return type should indicate that the value should be disposed", Justification = "items created by the factory are disposed by the pool or the factory")] + public IEcho CreateItem() => new Echo(); + + public ValueTask IsReadyAsync( + string connectionKey, + IEcho item, + CancellationToken cancellationToken) => ValueTask.FromResult(item.IsConnected); + + public Task PrepareAsync( + string connectionKey, + IEcho item, + CancellationToken cancellationToken) => item.ConnectAsync(cancellationToken); +} diff --git a/src/Pool.Tests/PoolMapTests.cs b/src/Pool.Tests/PoolMapTests.cs new file mode 100644 index 0000000..b74a584 --- /dev/null +++ b/src/Pool.Tests/PoolMapTests.cs @@ -0,0 +1,65 @@ + +using Pool.Tests.Fakes; + +namespace Pool.Tests; +public sealed class PoolMapTests(IPoolMap pool) +{ + + [Fact] + public void Pool_Is_Injected() => Assert.NotNull(pool); + + [Fact] + public async Task Lease_And_Release() + { + Assert.Equal(0, pool.UniqueLeases); + var instance = await pool.LeaseAsync("testing", CancellationToken.None); + Assert.NotNull(instance); + Assert.Equal(1, pool.UniqueLeases); + await pool.ReleaseAsync("testing", instance, CancellationToken.None); + } + + [Fact] + public async Task Lease_Unique_Request() + { + var instance1 = await pool.LeaseAsync("testing", CancellationToken.None); + Assert.Equal(1, pool.UniqueLeases); + + var task = pool.LeaseAsync("testing", CancellationToken.None); + Assert.Equal(1, pool.UniqueLeases); + + await pool.ReleaseAsync("testing", instance1, CancellationToken.None); + Assert.Equal(1, pool.UniqueLeases); + + var instance2 = await task; + Assert.NotNull(instance2); + + await pool.ReleaseAsync("testing", instance2, CancellationToken.None); + } + + [Fact] + public async Task Lease_Returns_Ready_Item() + { + var instance = await pool.LeaseAsync("testing", CancellationToken.None); + + Assert.True(instance.IsConnected); + + await pool.ReleaseAsync("testing", instance, CancellationToken.None); + } + + [Fact] + public async Task Queued_Request_Timesout() + { + var instance1 = await pool.LeaseAsync("testing", CancellationToken.None); + Assert.Equal(1, pool.UniqueLeases); + try + { + var execption = await Assert + .ThrowsAsync(async () => + await pool.LeaseAsync("testing", CancellationToken.None)); + } + finally + { + await pool.ReleaseAsync("testing", instance1, CancellationToken.None); + } + } +} diff --git a/src/Pool.Tests/Startup.cs b/src/Pool.Tests/Startup.cs index bdd0e3b..f687a10 100644 --- a/src/Pool.Tests/Startup.cs +++ b/src/Pool.Tests/Startup.cs @@ -20,5 +20,5 @@ public sealed class Startup .Build(); public void ConfigureServices(IServiceCollection services) => _ = services - .AddTestPool(configuration); + .AddTestPool(configuration); } diff --git a/src/Pool/DefaultStrategies/DefaultPreparationStrategy{TKey TPool} .cs b/src/Pool/DefaultStrategies/DefaultPreparationStrategy{TKey TPool} .cs new file mode 100644 index 0000000..b2d7371 --- /dev/null +++ b/src/Pool/DefaultStrategies/DefaultPreparationStrategy{TKey TPool} .cs @@ -0,0 +1,10 @@ +namespace Pool.DefaultStrategies; + +internal sealed class DefaultPreparationStrategy : IPreparationStrategy + where TKey : class + where TPool : class +{ + public ValueTask IsReadyAsync(TKey connectionKey, TPool item, CancellationToken cancellationToken) => ValueTask.FromResult(true); + + public Task PrepareAsync(TKey connectionKey, TPool item, CancellationToken cancellationToken) => Task.CompletedTask; +} diff --git a/src/Pool/IItemFactory{TPoolItem}.cs b/src/Pool/IItemFactory{TPoolItem}.cs index 36680bf..d59d74b 100644 --- a/src/Pool/IItemFactory{TPoolItem}.cs +++ b/src/Pool/IItemFactory{TPoolItem}.cs @@ -3,7 +3,7 @@ namespace Pool; /// -/// IPoolItemFactory creates pool items. +/// IPoolItemFactory creates pool pools. /// /// /// Implement your own factory, or use the . @@ -13,6 +13,6 @@ public interface IItemFactory /// /// CreateItem returns a new pool item instance. /// - /// TPoolItem + /// TPool TPoolItem CreateItem(); } diff --git a/src/Pool/IPoolMap{TKey TPool}.cs b/src/Pool/IPoolMap{TKey TPool}.cs new file mode 100644 index 0000000..f935267 --- /dev/null +++ b/src/Pool/IPoolMap{TKey TPool}.cs @@ -0,0 +1,62 @@ +namespace Pool; + +/// +/// pool +/// +/// +/// +public interface IPoolMap + where TKey : class + where TPool : class +{ + /// + /// clears the pool and sets allocated to zero + /// + Task ClearAsync(); + + /// + /// clears the pool and sets allocated to zero + /// + Task ClearAsync(CancellationToken cancellationToken); + + /// + /// Simple lease. + /// Check the connection pool, to see if there is an existing entry for that connection key, if there is one, it retrieves an item from the pool + /// identified by that connection, if there is no entry for that key it will create a new one and retrieve the item from the pool this is while there is capacity available + /// for that pool, if there is no pools available for that request it waits forever. + /// waits forever. + /// + /// item from the pool + ValueTask LeaseAsync(TKey key); + + /// + /// Simple lease. + /// Check the connection pool, to see if there is an existing entry for that connection key, if there is one, it retrieves an item from the pool + /// identified by that connection, if there is no entry for that key it will create a new one and retrieve the item from the pool this is while there is capacity available + /// for that pool, if there is no pools available for that request it waits forever. + /// + /// + /// + /// item from the pool + ValueTask LeaseAsync(TKey key, CancellationToken cancellationToken); + + /// + /// returns a pool identfied by the key + /// + /// + /// + Task ReleaseAsync(TKey key, TPool pool); + + /// + /// returns an item to the pool that is identified by the key + /// + /// + /// + /// + Task ReleaseAsync(TKey key, TPool pool, CancellationToken cancellationToken); + + /// + /// returns how many pools are currently leased + /// + int UniqueLeases { get; } +} diff --git a/src/Pool/IPool{TPoolItem}.cs b/src/Pool/IPool{TPoolItem}.cs index 454601f..86dd477 100644 --- a/src/Pool/IPool{TPoolItem}.cs +++ b/src/Pool/IPool{TPoolItem}.cs @@ -47,17 +47,17 @@ public interface IPool Task ReleaseAsync(TPoolItem item, CancellationToken cancellationToken); /// - /// returns how many items are currently allocated by the pool + /// returns how many pools are currently allocated by the pool /// int ItemsAllocated { get; } /// - /// returns the how many items are of allocated but not leased + /// returns the how many pools are of allocated but not leased /// int ItemsAvailable { get; } /// - /// returns how many items are currently leased + /// returns how many pools are currently leased /// int ActiveLeases { get; } diff --git a/src/Pool/IPreparationStrategy{TConnectionKey TPool}.cs b/src/Pool/IPreparationStrategy{TConnectionKey TPool}.cs new file mode 100644 index 0000000..6df574d --- /dev/null +++ b/src/Pool/IPreparationStrategy{TConnectionKey TPool}.cs @@ -0,0 +1,29 @@ +namespace Pool; +/// +/// IPreparationStrategy is an interface for preparing pool pools before the pool leases them to the caller. +/// +/// +/// +public interface IPreparationStrategy + where TKey : class + where TPool : class +{ + /// + /// IsReadyAsync checks if the pool item is ready before the pool leases it to the caller. + /// + /// + /// + /// + /// true if the pool item is ready. + ValueTask IsReadyAsync(TKey key, TPool pool, CancellationToken cancellationToken); + + /// + /// PrepareAsync makes the pool item ready before the pool leases it to the caller. + /// + /// + /// + /// + /// + /// The pool will call PrepareAsync when IsReadyAsync returns false. Implement PrepareAsync to initialize an object, or establish a connection, like connecting to a database or smtp server. + Task PrepareAsync(TKey key, TPool pool, CancellationToken cancellationToken); +} diff --git a/src/Pool/IPreparationStrategy{TPoolItem}.cs b/src/Pool/IPreparationStrategy{TPoolItem}.cs index 2ae0e30..e03bdcd 100644 --- a/src/Pool/IPreparationStrategy{TPoolItem}.cs +++ b/src/Pool/IPreparationStrategy{TPoolItem}.cs @@ -1,7 +1,7 @@ namespace Pool; /// -/// IPreparationStrategy is an interface for preparing pool items before the pool leases them to the caller. +/// IPreparationStrategy is an interface for preparing pool pools before the pool leases them to the caller. /// /// public interface IPreparationStrategy diff --git a/src/Pool/PoolMap{TKey TPool}.cs b/src/Pool/PoolMap{TKey TPool}.cs new file mode 100644 index 0000000..31bfdfe --- /dev/null +++ b/src/Pool/PoolMap{TKey TPool}.cs @@ -0,0 +1,152 @@ +using System.Collections.Concurrent; +using System.Runtime.CompilerServices; + +namespace Pool; + +/// > +public sealed class PoolMap + : IPoolMap + , IDisposable + where TKey : class + where TPool : class +{ + + private static readonly bool IsPoolItemDisposable = typeof(TPool).GetInterface(nameof(IDisposable), true) is not null; + + private readonly ConcurrentDictionary> pools = new(); + private readonly PoolOptions poolOptions; + private readonly bool preparationRequired; + private readonly IItemFactory itemFactory; + private readonly IPreparationStrategy? preparationStrategy; + private readonly IPreparationStrategy? connectionpreparationStrategy; + private readonly TimeSpan preparationTimeout; + private bool disposed; + + /// + /// ctor + /// + /// + /// + public PoolMap( + IItemFactory itemFactory, + PoolOptions options) + : this(itemFactory, null, null, options) + { } + + /// + /// ctor + /// + /// + /// + /// + /// + /// + public PoolMap( + IItemFactory itemFactory, + IPreparationStrategy? preparationStrategy, + IPreparationStrategy? connectionpreparationStrategy, + PoolOptions options) + { + this.itemFactory = itemFactory ?? throw new ArgumentNullException(nameof(itemFactory)); + + preparationRequired = preparationStrategy is not null; + this.preparationStrategy = preparationStrategy; + this.connectionpreparationStrategy = connectionpreparationStrategy; + poolOptions = options; + preparationTimeout = options?.PreparationTimeout ?? Timeout.InfiniteTimeSpan; + } + + /// > + public int UniqueLeases { get; private set; } + + /// > + public int QueuedLeases => pools.Select(x => x.Value).Select(x => x.QueuedLeases).Sum(); + + /// > + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ValueTask LeaseAsync(TKey key) => LeaseAsync(key, CancellationToken.None); + + /// > + public async ValueTask LeaseAsync(TKey key, CancellationToken cancellationToken) + { + _ = ThrowIfDisposed().TryAcquireItem(key, out var item); + + var pooItem = await LeasePoolItemAsync(item, cancellationToken); + return await EnsurePreparedAsync(key, pooItem, cancellationToken); + } + + /// > + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Task ReleaseAsync(TKey key, TPool pool) => ReleaseAsync(key, pool, CancellationToken.None); + + /// > + public async Task ReleaseAsync( + TKey key, + TPool pool, + CancellationToken cancellationToken) => await pools[key].ReleaseAsync(pool, cancellationToken); + + /// > + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Task ClearAsync() => ClearAsync(CancellationToken.None); + + /// > + public async Task ClearAsync(CancellationToken cancellationToken) + => await Task.WhenAll(pools.Select(x => x.Value.ClearAsync(cancellationToken))); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool TryAcquireItem(TKey connectionKey, out Pool item) => + TryGetItem(connectionKey, out item) || TryCreateItem(connectionKey, out item); + + private bool TryCreateItem(TKey connectionKey, out Pool item) + { + lock (this) + { + item = new Pool(itemFactory, preparationStrategy, poolOptions); + ++UniqueLeases; + _ = pools.TryAdd(connectionKey, item); + return true; + } + } + + private bool TryGetItem(TKey connectionKey, out Pool item) => pools.TryGetValue(connectionKey, out item!); + + private static async ValueTask LeasePoolItemAsync(Pool pool, CancellationToken cancellationToken) + => await pool.LeaseAsync(cancellationToken); + + private async ValueTask EnsurePreparedAsync( + TKey connectionKey, + TPool item, + CancellationToken cancellationToken) + { + using var timeoutCts = new CancellationTokenSource(preparationTimeout); + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource( + timeoutCts.Token, + cancellationToken); + cancellationToken = linkedCts.Token; + + if (await connectionpreparationStrategy!.IsReadyAsync(connectionKey, item, cancellationToken)) + { + return item; + } + + await connectionpreparationStrategy.PrepareAsync(connectionKey, item, cancellationToken); + + return item; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private PoolMap ThrowIfDisposed() => disposed + ? throw new ObjectDisposedException(nameof(PoolMap)) + : this; + + /// + public void Dispose() + { + if (disposed) + { + return; + } + + disposed = true; + } +} diff --git a/src/Pool/PoolOptions.cs b/src/Pool/PoolOptions.cs index 0ff2d30..0a662f1 100644 --- a/src/Pool/PoolOptions.cs +++ b/src/Pool/PoolOptions.cs @@ -6,13 +6,13 @@ public sealed class PoolOptions { /// - /// MinSize gets or sets the minimum number of items in the pool. + /// MinSize gets or sets the minimum number of pools in the pool. /// /// Defaults to zero. public int MinSize { get; set; } /// - /// MaxSize gets or sets the maximum number of items in the pool. + /// MaxSize gets or sets the maximum number of pools in the pool. /// /// Defaults to Int32.MaxValue public int MaxSize { get; set; } = Int32.MaxValue; diff --git a/src/Pool/ServiceCollectionExtensions.cs b/src/Pool/ServiceCollectionExtensions.cs index d7a5b87..e4970cf 100644 --- a/src/Pool/ServiceCollectionExtensions.cs +++ b/src/Pool/ServiceCollectionExtensions.cs @@ -104,13 +104,19 @@ public static IServiceCollection AddPool( [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "the case is handled in the conditional compile directives above")] internal static IServiceCollection AddTestPool< TPoolItem, + TPool, + TKey, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TFactoryImplementation, - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TPreparationStrategy>( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TPreparationStrategy, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TPoolMapPreparationStrategy>( this IServiceCollection services, IConfiguration configuration) where TPoolItem : class + where TPool : class + where TKey : class where TFactoryImplementation : class, IItemFactory where TPreparationStrategy : class, IPreparationStrategy + where TPoolMapPreparationStrategy : class, IPreparationStrategy { ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(configuration); @@ -118,7 +124,9 @@ internal static IServiceCollection AddTestPool< services.TryAddSingleton(configuration.GetSection(nameof(PoolOptions)).Get() ?? new PoolOptions()); services.TryAddTransient, TFactoryImplementation>(); services.TryAddTransient, TPreparationStrategy>(); + services.TryAddTransient, TPoolMapPreparationStrategy>(); services.TryAddTransient, Pool>(); + services.TryAddTransient, PoolMap>(); return services; }