From 0742eccbb28e316d418f2a6ccc9afb4b65198b85 Mon Sep 17 00:00:00 2001 From: James Gunn Date: Thu, 17 Oct 2024 14:13:04 +0100 Subject: [PATCH] WIP: CRM connection pool --- .../src/TeachingRecordSystem.Api/Program.cs | 2 +- .../Dqt/PooledOrganizationService.cs | 149 ++++++++++++++++++ .../Dqt/ServiceCollectionExtensions.cs | 37 ++++- 3 files changed, 181 insertions(+), 7 deletions(-) create mode 100644 TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/PooledOrganizationService.cs diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Api/Program.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Api/Program.cs index 857ecc67f..ca6ec8729 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Api/Program.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Api/Program.cs @@ -220,7 +220,7 @@ public static void Main(string[] args) { var crmServiceClient = GetCrmServiceClient(); services.AddTrnGenerationApi(configuration); - services.AddDefaultServiceClient(ServiceLifetime.Transient, _ => crmServiceClient.Clone()); + services.AddPooledDefaultServiceClient(crmServiceClient, size: 200); services.AddTransient(); services.AddHealthChecks() diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/PooledOrganizationService.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/PooledOrganizationService.cs new file mode 100644 index 000000000..d52641888 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/PooledOrganizationService.cs @@ -0,0 +1,149 @@ +using System.Collections.Concurrent; +using Microsoft.PowerPlatform.Dataverse.Client; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Query; + +namespace TeachingRecordSystem.Core.Dqt; + +internal sealed class PooledOrganizationService : IOrganizationServiceAsync2, IDisposable +{ + private readonly BlockingCollection _pool; + private bool _disposed; + + private PooledOrganizationService(IEnumerable connections) + { + _pool = new BlockingCollection(new ConcurrentQueue(connections)); + } + + public static PooledOrganizationService Create(ServiceClient serviceClient, int size) + { + var connections = new List(size); + connections.AddRange(Enumerable.Range(0, size).Select(_ => serviceClient.Clone())); + return new PooledOrganizationService(connections); + } + + public void Dispose() + { + _pool.Dispose(); + _disposed = true; + } + + private async Task WithPooledConnectionAsync(Func> action, CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + var client = _pool.Take(cancellationToken); + try + { + return await action(client); + } + finally + { + _pool.Add(client); + } + } + + private async Task WithPooledConnectionAsync(Func action, CancellationToken cancellationToken = default) => + await WithPooledConnectionAsync(async client => + { + await action(client); + return 1; + }); + + private TResult WithPooledConnection(Func action) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + var client = _pool.Take(); + try + { + return action(client); + } + finally + { + _pool.Add(client); + } + } + + private void WithPooledConnection(Action action) => + WithPooledConnection(client => + { + action(client); + return 1; + }); + + public Task AssociateAsync(string entityName, Guid entityId, Relationship relationship, EntityReferenceCollection relatedEntities, CancellationToken cancellationToken) => + WithPooledConnectionAsync(s => s.AssociateAsync(entityName, entityId, relationship, relatedEntities, cancellationToken), cancellationToken); + + public Task CreateAsync(Entity entity, CancellationToken cancellationToken) => + WithPooledConnectionAsync(s => s.CreateAsync(entity, cancellationToken), cancellationToken); + + public Task CreateAndReturnAsync(Entity entity, CancellationToken cancellationToken) => + WithPooledConnectionAsync(s => s.CreateAndReturnAsync(entity, cancellationToken), cancellationToken); + + public Task DeleteAsync(string entityName, Guid id, CancellationToken cancellationToken) => + WithPooledConnectionAsync(s => s.DeleteAsync(entityName, id, cancellationToken), cancellationToken); + + public Task DisassociateAsync(string entityName, Guid entityId, Relationship relationship, EntityReferenceCollection relatedEntities, CancellationToken cancellationToken) => + WithPooledConnectionAsync(s => s.DisassociateAsync(entityName, entityId, relationship, relatedEntities, cancellationToken), cancellationToken); + + public Task ExecuteAsync(OrganizationRequest request, CancellationToken cancellationToken) => + WithPooledConnectionAsync(s => s.ExecuteAsync(request, cancellationToken), cancellationToken); + + public Task RetrieveAsync(string entityName, Guid id, ColumnSet columnSet, CancellationToken cancellationToken) => + WithPooledConnectionAsync(s => s.RetrieveAsync(entityName, id, columnSet, cancellationToken), cancellationToken); + + public Task RetrieveMultipleAsync(QueryBase query, CancellationToken cancellationToken) => + WithPooledConnectionAsync(s => s.RetrieveMultipleAsync(query, cancellationToken), cancellationToken); + + public Task UpdateAsync(Entity entity, CancellationToken cancellationToken) => + WithPooledConnectionAsync(s => s.UpdateAsync(entity, cancellationToken), cancellationToken); + + public Task CreateAsync(Entity entity) => + WithPooledConnectionAsync(s => s.CreateAsync(entity)); + + public Task RetrieveAsync(string entityName, Guid id, ColumnSet columnSet) => + WithPooledConnectionAsync(s => s.RetrieveAsync(entityName, id, columnSet)); + + public Task UpdateAsync(Entity entity) => + WithPooledConnectionAsync(s => s.UpdateAsync(entity)); + + public Task DeleteAsync(string entityName, Guid id) => + WithPooledConnectionAsync(s => s.DeleteAsync(entityName, id)); + + public Task ExecuteAsync(OrganizationRequest request) => + WithPooledConnectionAsync(s => s.ExecuteAsync(request)); + + public Task AssociateAsync(string entityName, Guid entityId, Relationship relationship, EntityReferenceCollection relatedEntities) => + WithPooledConnectionAsync(s => s.AssociateAsync(entityName, entityId, relationship, relatedEntities)); + + public Task DisassociateAsync(string entityName, Guid entityId, Relationship relationship, EntityReferenceCollection relatedEntities) => + WithPooledConnectionAsync(s => s.DisassociateAsync(entityName, entityId, relationship, relatedEntities)); + + public Task RetrieveMultipleAsync(QueryBase query) => + WithPooledConnectionAsync(s => s.RetrieveMultipleAsync(query)); + + public Guid Create(Entity entity) => + WithPooledConnection(s => s.Create(entity)); + + public Entity Retrieve(string entityName, Guid id, ColumnSet columnSet) => + WithPooledConnection(s => s.Retrieve(entityName, id, columnSet)); + + public void Update(Entity entity) => + WithPooledConnection(s => s.Update(entity)); + + public void Delete(string entityName, Guid id) => + WithPooledConnection(s => s.Delete(entityName, id)); + + public OrganizationResponse Execute(OrganizationRequest request) => + WithPooledConnection(s => s.Execute(request)); + + public void Associate(string entityName, Guid entityId, Relationship relationship, EntityReferenceCollection relatedEntities) => + WithPooledConnection(s => s.Associate(entityName, entityId, relationship, relatedEntities)); + + public void Disassociate(string entityName, Guid entityId, Relationship relationship, EntityReferenceCollection relatedEntities) => + WithPooledConnection(s => s.Disassociate(entityName, entityId, relationship, relatedEntities)); + + public EntityCollection RetrieveMultiple(QueryBase query) => + WithPooledConnection(s => s.RetrieveMultiple(query)); +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/ServiceCollectionExtensions.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/ServiceCollectionExtensions.cs index f32a78cbb..30d280c9f 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/ServiceCollectionExtensions.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/ServiceCollectionExtensions.cs @@ -21,12 +21,37 @@ public static IServiceCollection AddCrmQueries(this IServiceCollection services) return services; } + public static IServiceCollection AddPooledDefaultServiceClient( + this IServiceCollection services, + ServiceClient baseServiceClient, + int size) + { + services.AddKeyedSingleton(serviceKey: null, PooledOrganizationService.Create(baseServiceClient, size)); + + services.AddDefaultServiceClient(ServiceLifetime.Singleton, sp => sp.GetRequiredKeyedService(serviceKey: null)); + + return services; + } + + public static IServiceCollection AddPooledNamedServiceClient( + this IServiceCollection services, + string name, + ServiceClient baseServiceClient, + int size) + { + services.AddKeyedSingleton(name, PooledOrganizationService.Create(baseServiceClient, size)); + + services.AddNamedServiceClient(name, ServiceLifetime.Singleton, sp => sp.GetRequiredKeyedService(name)); + + return services; + } + public static IServiceCollection AddDefaultServiceClient( this IServiceCollection services, ServiceLifetime lifetime, - Func createServiceClient) + Func getServiceClient) { - services.AddServiceClient(name: null, lifetime, createServiceClient); + services.AddServiceClient(name: null, lifetime, getServiceClient); services.AddSingleton(); @@ -37,9 +62,9 @@ public static IServiceCollection AddNamedServiceClient( this IServiceCollection services, string name, ServiceLifetime lifetime, - Func createServiceClient) + Func getServiceClient) { - return AddServiceClient(services, name, lifetime, createServiceClient); + return AddServiceClient(services, name, lifetime, getServiceClient); } public static IServiceCollection AddCrmEntityChangesService( @@ -73,14 +98,14 @@ private static IServiceCollection AddServiceClient( this IServiceCollection services, string? name, ServiceLifetime lifetime, - Func createServiceClient) + Func getServiceClient) { services.TryAddSingleton(); services.Add(ServiceDescriptor.DescribeKeyed( typeof(IOrganizationServiceAsync2), name, - implementationFactory: (IServiceProvider serviceProvider, object? key) => createServiceClient(serviceProvider), + implementationFactory: (IServiceProvider serviceProvider, object? key) => getServiceClient(serviceProvider), lifetime)); services.Add(ServiceDescriptor.DescribeKeyed(