Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/Pool.Tests/Fakes/EchoMapFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@

namespace Pool.Tests.Fakes;

internal sealed class EchoMapFactory
: IItemFactory<IEcho>
, IPreparationStrategy<string, IEcho>
{
[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<bool> IsReadyAsync(
string connectionKey,
IEcho item,
CancellationToken cancellationToken) => ValueTask.FromResult(item.IsConnected);

public Task PrepareAsync(
string connectionKey,
IEcho item,
CancellationToken cancellationToken) => item.ConnectAsync(cancellationToken);
}
65 changes: 65 additions & 0 deletions src/Pool.Tests/PoolMapTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@

using Pool.Tests.Fakes;

namespace Pool.Tests;
public sealed class PoolMapTests(IPoolMap<string, IEcho> 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<TaskCanceledException>(async () =>
await pool.LeaseAsync("testing", CancellationToken.None));
}
finally
{
await pool.ReleaseAsync("testing", instance1, CancellationToken.None);
}
}
}
2 changes: 1 addition & 1 deletion src/Pool.Tests/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@ public sealed class Startup
.Build();

public void ConfigureServices(IServiceCollection services) => _ = services
.AddTestPool<IEcho, EchoFactory, EchoFactory>(configuration);
.AddTestPool<IEcho, IEcho, string, EchoFactory, EchoFactory, EchoMapFactory>(configuration);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Pool.DefaultStrategies;

internal sealed class DefaultPreparationStrategy<TKey, TPool> : IPreparationStrategy<TKey, TPool>
where TKey : class
where TPool : class
{
public ValueTask<bool> IsReadyAsync(TKey connectionKey, TPool item, CancellationToken cancellationToken) => ValueTask.FromResult(true);

public Task PrepareAsync(TKey connectionKey, TPool item, CancellationToken cancellationToken) => Task.CompletedTask;
}
4 changes: 2 additions & 2 deletions src/Pool/IItemFactory{TPoolItem}.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
namespace Pool;

/// <summary>
/// IPoolItemFactory creates pool items.
/// IPoolItemFactory creates pool pools.
/// </summary>
/// <typeparam name="TPoolItem"></typeparam>
/// <remarks>Implement your own factory, or use the <see cref="DefaultItemFactory{TPoolItem}"/>.</remarks>
Expand All @@ -13,6 +13,6 @@ public interface IItemFactory<TPoolItem>
/// <summary>
/// CreateItem returns a new pool item instance.
/// </summary>
/// <returns>TPoolItem</returns>
/// <returns>TPool</returns>
TPoolItem CreateItem();
}
62 changes: 62 additions & 0 deletions src/Pool/IPoolMap{TKey TPool}.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
namespace Pool;

/// <summary>
/// pool
/// </summary>
/// <typeparam name="TKey"></typeparam>
/// <typeparam name="TPool"></typeparam>
public interface IPoolMap<TKey, TPool>
where TKey : class
where TPool : class
{
/// <summary>
/// clears the pool and sets allocated to zero
/// </summary>
Task ClearAsync();

/// <summary>
/// clears the pool and sets allocated to zero
/// </summary>
Task ClearAsync(CancellationToken cancellationToken);

/// <summary>
/// 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.
/// </summary>
/// <returns>item from the pool</returns>
ValueTask<TPool> LeaseAsync(TKey key);

/// <summary>
/// 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.
/// </summary>
/// <param name="key"></param>
/// <param name="cancellationToken"></param>
/// <returns>item from the pool</returns>
ValueTask<TPool> LeaseAsync(TKey key, CancellationToken cancellationToken);

/// <summary>
/// returns a pool identfied by the key
/// </summary>
/// <param name="key"></param>
/// <param name="pool"></param>
Task ReleaseAsync(TKey key, TPool pool);

/// <summary>
/// returns an item to the pool that is identified by the key
/// </summary>
/// <param name="key"></param>
/// <param name="pool"></param>
/// <param name="cancellationToken"></param>
Task ReleaseAsync(TKey key, TPool pool, CancellationToken cancellationToken);

/// <summary>
/// returns how many pools are currently leased
/// </summary>
int UniqueLeases { get; }
}
6 changes: 3 additions & 3 deletions src/Pool/IPool{TPoolItem}.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,17 @@ public interface IPool<TPoolItem>
Task ReleaseAsync(TPoolItem item, CancellationToken cancellationToken);

/// <summary>
/// returns how many items are currently allocated by the pool
/// returns how many pools are currently allocated by the pool
/// </summary>
int ItemsAllocated { get; }

/// <summary>
/// returns the how many items are of allocated but not leased
/// returns the how many pools are of allocated but not leased
/// </summary>
int ItemsAvailable { get; }

/// <summary>
/// returns how many items are currently leased
/// returns how many pools are currently leased
/// </summary>
int ActiveLeases { get; }

Expand Down
29 changes: 29 additions & 0 deletions src/Pool/IPreparationStrategy{TConnectionKey TPool}.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
namespace Pool;
/// <summary>
/// IPreparationStrategy is an interface for preparing pool pools before the pool leases them to the caller.
/// </summary>
/// <typeparam name="TKey"></typeparam>
/// <typeparam name="TPool"></typeparam>
public interface IPreparationStrategy<TKey, TPool>
where TKey : class
where TPool : class
{
/// <summary>
/// IsReadyAsync checks if the pool item is ready before the pool leases it to the caller.
/// </summary>
/// <param name="key"></param>
/// <param name="pool"></param>
/// <param name="cancellationToken"></param>
/// <returns><see cref="Boolean"/> true if the pool item is ready.</returns>
ValueTask<bool> IsReadyAsync(TKey key, TPool pool, CancellationToken cancellationToken);

/// <summary>
/// PrepareAsync makes the pool item ready before the pool leases it to the caller.
/// </summary>
/// <param name="key"></param>
/// <param name="pool"></param>
/// <param name="cancellationToken"></param>
/// <returns><see cref="Task"/></returns>
/// <remarks>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.</remarks>
Task PrepareAsync(TKey key, TPool pool, CancellationToken cancellationToken);
}
2 changes: 1 addition & 1 deletion src/Pool/IPreparationStrategy{TPoolItem}.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
namespace Pool;

/// <summary>
/// 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.
/// </summary>
/// <typeparam name="TPoolItem"></typeparam>
public interface IPreparationStrategy<TPoolItem>
Expand Down
152 changes: 152 additions & 0 deletions src/Pool/PoolMap{TKey TPool}.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
using System.Collections.Concurrent;
using System.Runtime.CompilerServices;

namespace Pool;

/// <inheritdoc/>>
public sealed class PoolMap<TKey, TPool>
: IPoolMap<TKey, TPool>
, IDisposable
where TKey : class
where TPool : class
{

private static readonly bool IsPoolItemDisposable = typeof(TPool).GetInterface(nameof(IDisposable), true) is not null;

private readonly ConcurrentDictionary<TKey, Pool<TPool>> pools = new();
private readonly PoolOptions poolOptions;
private readonly bool preparationRequired;
private readonly IItemFactory<TPool> itemFactory;
private readonly IPreparationStrategy<TPool>? preparationStrategy;
private readonly IPreparationStrategy<TKey, TPool>? connectionpreparationStrategy;
private readonly TimeSpan preparationTimeout;
private bool disposed;

/// <summary>
/// ctor
/// </summary>
/// <param name="itemFactory"></param>
/// <param name="options"></param>
public PoolMap(
IItemFactory<TPool> itemFactory,
PoolOptions options)
: this(itemFactory, null, null, options)
{ }

/// <summary>
/// ctor
/// </summary>
/// <param name="itemFactory"><see cref="IItemFactory{TPoolItem}"/></param>
/// <param name="preparationStrategy"><see cref="IPreparationStrategy{TPoolItem}"/></param>
/// <param name="connectionpreparationStrategy"><see cref="IPreparationStrategy{TConnectionKeyTPoolItem}"/></param>
/// <param name="options"><see cref="PoolOptions"/></param>
/// <exception cref="ArgumentNullException"></exception>
public PoolMap(
IItemFactory<TPool> itemFactory,
IPreparationStrategy<TPool>? preparationStrategy,
IPreparationStrategy<TKey, TPool>? 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;
}

/// <inheritdoc/>>
public int UniqueLeases { get; private set; }

/// <inheritdoc/>>
public int QueuedLeases => pools.Select(x => x.Value).Select(x => x.QueuedLeases).Sum();

/// <inheritdoc/>>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ValueTask<TPool> LeaseAsync(TKey key) => LeaseAsync(key, CancellationToken.None);

/// <inheritdoc/>>
public async ValueTask<TPool> LeaseAsync(TKey key, CancellationToken cancellationToken)
{
_ = ThrowIfDisposed().TryAcquireItem(key, out var item);

var pooItem = await LeasePoolItemAsync(item, cancellationToken);
return await EnsurePreparedAsync(key, pooItem, cancellationToken);
}

/// <inheritdoc/>>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Task ReleaseAsync(TKey key, TPool pool) => ReleaseAsync(key, pool, CancellationToken.None);

/// <inheritdoc/>>
public async Task ReleaseAsync(
TKey key,
TPool pool,
CancellationToken cancellationToken) => await pools[key].ReleaseAsync(pool, cancellationToken);

/// <inheritdoc/>>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Task ClearAsync() => ClearAsync(CancellationToken.None);

/// <inheritdoc/>>
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<TPool> item) =>
TryGetItem(connectionKey, out item) || TryCreateItem(connectionKey, out item);

private bool TryCreateItem(TKey connectionKey, out Pool<TPool> item)
{
lock (this)
{
item = new Pool<TPool>(itemFactory, preparationStrategy, poolOptions);
++UniqueLeases;
_ = pools.TryAdd(connectionKey, item);
return true;
}
}

private bool TryGetItem(TKey connectionKey, out Pool<TPool> item) => pools.TryGetValue(connectionKey, out item!);

private static async ValueTask<TPool> LeasePoolItemAsync(Pool<TPool> pool, CancellationToken cancellationToken)
=> await pool.LeaseAsync(cancellationToken);

private async ValueTask<TPool> 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<TKey, TPool> ThrowIfDisposed() => disposed
? throw new ObjectDisposedException(nameof(PoolMap<TKey, TPool>))
: this;

/// <inheritdoc/>
public void Dispose()
{
if (disposed)
{
return;
}

disposed = true;
}
}
Loading