Skip to content

Commit

Permalink
Merge pull request #655 from PinguApps/feature/improve-mem-perf
Browse files Browse the repository at this point in the history
Possible mem perf improvements
  • Loading branch information
pingu2k4 authored Feb 4, 2025
2 parents f4fbea6 + dc02015 commit bd20487
Show file tree
Hide file tree
Showing 3 changed files with 162 additions and 190 deletions.
221 changes: 110 additions & 111 deletions src/PinguApps.Appwrite.Client/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Logging;
using PinguApps.Appwrite.Client.Clients;
using PinguApps.Appwrite.Client.Handlers;
Expand All @@ -19,7 +18,7 @@
namespace PinguApps.Appwrite.Client;

/// <summary>
/// Provides extenions to IServiceCollection, to enable adding the SDK to your DI container
/// Provides extensions to IServiceCollection, to enable adding the SDK to your DI container
/// </summary>
public static class ServiceCollectionExtensions
{
Expand All @@ -38,52 +37,22 @@ public static IServiceCollection AddAppwriteClient(this IServiceCollection servi
var policyOptions = new ResiliencePolicyOptions();
configureResiliencePolicy?.Invoke(policyOptions);

var customRefitSettings = AddSerializationConfigToRefitSettings(refitSettings);

services.AddKeyedSingleton("Client", new Config(endpoint, projectId));

services.AddSingleton<IAsyncPolicy<HttpResponseMessage>>(sp =>
CreateResiliencePolicy(sp.GetRequiredService<ILogger<ResiliencePolicy>>(), policyOptions));

services.AddTransient<HeaderHandler>();
services.AddTransient<ClientCookieSessionHandler>();

services.AddRefitClient<IAccountApi>(customRefitSettings)
.ConfigureHttpClient(x => ConfigureHttpClient(x, endpoint))
.AddHttpMessageHandler<HeaderHandler>()
.AddHttpMessageHandler(() => new PolicyHttpMessageHandler(GetRetryPolicy<IAccountApi>(services, policyOptions)))
.AddHttpMessageHandler(() => new PolicyHttpMessageHandler(GetCircuitBreakerPolicy<IAccountApi>(services, policyOptions)))
.AddHttpMessageHandler<ClientCookieSessionHandler>();

services.AddRefitClient<ITeamsApi>(customRefitSettings)
.ConfigureHttpClient(x => ConfigureHttpClient(x, endpoint))
.AddHttpMessageHandler<HeaderHandler>()
.AddHttpMessageHandler(() => new PolicyHttpMessageHandler(GetRetryPolicy<ITeamsApi>(services, policyOptions)))
.AddHttpMessageHandler(() => new PolicyHttpMessageHandler(GetCircuitBreakerPolicy<ITeamsApi>(services, policyOptions)))
.AddHttpMessageHandler<ClientCookieSessionHandler>();
var customRefitSettings = AddSerializationConfigToRefitSettings(refitSettings);

services.AddRefitClient<IDatabasesApi>(customRefitSettings)
.ConfigureHttpClient(x => ConfigureHttpClient(x, endpoint))
.AddHttpMessageHandler<HeaderHandler>()
.AddHttpMessageHandler(() => new PolicyHttpMessageHandler(GetRetryPolicy<IDatabasesApi>(services, policyOptions)))
.AddHttpMessageHandler(() => new PolicyHttpMessageHandler(GetCircuitBreakerPolicy<IDatabasesApi>(services, policyOptions)))
.AddHttpMessageHandler<ClientCookieSessionHandler>();
RegisterRefitClient<IAccountApi>(services, customRefitSettings, endpoint, true);
RegisterRefitClient<ITeamsApi>(services, customRefitSettings, endpoint, true);
RegisterRefitClient<IDatabasesApi>(services, customRefitSettings, endpoint, true);

services.AddSingleton<IClientAccountClient>(sp =>
{
var api = sp.GetRequiredService<IAccountApi>();
var config = sp.GetRequiredKeyedService<Config>("Client");
return new ClientAccountClient(api, config);
});
services.AddSingleton<IClientTeamsClient>(sp =>
{
var api = sp.GetRequiredService<ITeamsApi>();
var config = sp.GetRequiredKeyedService<Config>("Client");
return new ClientTeamsClient(api, config);
});
services.AddSingleton<IClientDatabasesClient>(sp =>
{
var api = sp.GetRequiredService<IDatabasesApi>();
return new ClientDatabasesClient(api);
});
services.AddSingleton<IClientAppwriteClient, ClientAppwriteClient>();
services.AddSingleton(x => new Lazy<IClientAppwriteClient>(() => x.GetRequiredService<IClientAppwriteClient>()));
// Register business logic clients
RegisterBusinessClients(services, true);

return services;
}
Expand All @@ -103,80 +72,121 @@ public static IServiceCollection AddAppwriteClientForServer(this IServiceCollect
var policyOptions = new ResiliencePolicyOptions();
configureResiliencePolicy?.Invoke(policyOptions);

var customRefitSettings = AddSerializationConfigToRefitSettings(refitSettings);

services.AddKeyedSingleton("Client", new Config(endpoint, projectId));

services.AddSingleton<IAsyncPolicy<HttpResponseMessage>>(sp =>
CreateResiliencePolicy(sp.GetRequiredService<ILogger<ResiliencePolicy>>(), policyOptions));

services.AddTransient<HeaderHandler>();

services.AddRefitClient<IAccountApi>(customRefitSettings)
.ConfigureHttpClient(x => ConfigureHttpClient(x, endpoint))
.AddHttpMessageHandler<HeaderHandler>()
.AddHttpMessageHandler(() => new PolicyHttpMessageHandler(GetRetryPolicy<IAccountApi>(services, policyOptions)))
.AddHttpMessageHandler(() => new PolicyHttpMessageHandler(GetCircuitBreakerPolicy<IAccountApi>(services, policyOptions)))
.ConfigurePrimaryHttpMessageHandler(ConfigurePrimaryHttpMessageHandler);
var customRefitSettings = AddSerializationConfigToRefitSettings(refitSettings);

services.AddRefitClient<ITeamsApi>(customRefitSettings)
.ConfigureHttpClient(x => ConfigureHttpClient(x, endpoint))
.AddHttpMessageHandler<HeaderHandler>()
.AddHttpMessageHandler(() => new PolicyHttpMessageHandler(GetRetryPolicy<ITeamsApi>(services, policyOptions)))
.AddHttpMessageHandler(() => new PolicyHttpMessageHandler(GetCircuitBreakerPolicy<ITeamsApi>(services, policyOptions)))
.ConfigurePrimaryHttpMessageHandler(ConfigurePrimaryHttpMessageHandler);
// Register API clients
RegisterRefitClient<IAccountApi>(services, customRefitSettings, endpoint, false);
RegisterRefitClient<ITeamsApi>(services, customRefitSettings, endpoint, false);
RegisterRefitClient<IDatabasesApi>(services, customRefitSettings, endpoint, false);

// Register business logic clients
RegisterBusinessClients(services, false);

services.AddRefitClient<IDatabasesApi>(customRefitSettings)
.ConfigureHttpClient(x => ConfigureHttpClient(x, endpoint))
return services;
}

private static void RegisterRefitClient<T>(IServiceCollection services, RefitSettings refitSettings,
string endpoint, bool includeSessionHandler)
where T : class
{
var builder = services.AddRefitClient<T>(refitSettings)
.ConfigureHttpClient(client =>
{
client.BaseAddress = new Uri(endpoint);
client.DefaultRequestHeaders.UserAgent.ParseAdd(BuildUserAgent());
client.Timeout = TimeSpan.FromSeconds(30);
})
.AddHttpMessageHandler<HeaderHandler>()
.AddHttpMessageHandler(() => new PolicyHttpMessageHandler(GetRetryPolicy<IDatabasesApi>(services, policyOptions)))
.AddHttpMessageHandler(() => new PolicyHttpMessageHandler(GetCircuitBreakerPolicy<IDatabasesApi>(services, policyOptions)))
.ConfigurePrimaryHttpMessageHandler(ConfigurePrimaryHttpMessageHandler);
.AddPolicyHandler((sp, _) => sp.GetRequiredService<IAsyncPolicy<HttpResponseMessage>>());

services.AddScoped<IClientAccountClient>(sp =>
if (includeSessionHandler)
{
builder.AddHttpMessageHandler<ClientCookieSessionHandler>();
}
else
{
builder.ConfigurePrimaryHttpMessageHandler(ConfigurePrimaryHttpMessageHandler);
}
}

private static void RegisterBusinessClients(IServiceCollection services, bool asSingleton)
{
if (asSingleton)
{
RegisterSingletonClients(services);
}
else
{
RegisterScopedClients(services);
}
}

private static void RegisterSingletonClients(IServiceCollection services)
{
services.AddSingleton<IClientAccountClient>(sp =>
{
var api = sp.GetRequiredService<IAccountApi>();
var config = sp.GetRequiredKeyedService<Config>("Client");
return new ClientAccountClient(api, config);
});
services.AddScoped<IClientTeamsClient>(sp =>

services.AddSingleton<IClientTeamsClient>(sp =>
{
var api = sp.GetRequiredService<ITeamsApi>();
var config = sp.GetRequiredKeyedService<Config>("Client");
return new ClientTeamsClient(api, config);
});
services.AddScoped<IClientDatabasesClient>(sp =>

services.AddSingleton<IClientDatabasesClient>(sp =>
{
var api = sp.GetRequiredService<IDatabasesApi>();
return new ClientDatabasesClient(api);
});
services.AddScoped<IClientAppwriteClient, ClientAppwriteClient>();

return services;
services.AddSingleton<IClientAppwriteClient, ClientAppwriteClient>();
services.AddSingleton(x => new Lazy<IClientAppwriteClient>(() =>
x.GetRequiredService<IClientAppwriteClient>()));
}

[ExcludeFromCodeCoverage]
private static void ConfigurePrimaryHttpMessageHandler(HttpMessageHandler messageHandler, IServiceProvider serviceProvider)
private static void RegisterScopedClients(IServiceCollection services)
{
if (messageHandler is HttpClientHandler clientHandler)
services.AddScoped<IClientAccountClient>(sp =>
{
clientHandler.UseCookies = false;
}
}
var api = sp.GetRequiredService<IAccountApi>();
var config = sp.GetRequiredKeyedService<Config>("Client");
return new ClientAccountClient(api, config);
});

private static void ConfigureHttpClient(HttpClient client, string endpoint)
{
client.BaseAddress = new Uri(endpoint);
client.DefaultRequestHeaders.UserAgent.ParseAdd(BuildUserAgent());
services.AddScoped<IClientTeamsClient>(sp =>
{
var api = sp.GetRequiredService<ITeamsApi>();
var config = sp.GetRequiredKeyedService<Config>("Client");
return new ClientTeamsClient(api, config);
});

services.AddScoped<IClientDatabasesClient>(sp =>
{
var api = sp.GetRequiredService<IDatabasesApi>();
return new ClientDatabasesClient(api);
});

services.AddScoped<IClientAppwriteClient, ClientAppwriteClient>();
}

[ExcludeFromCodeCoverage]
private static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy<T>(IServiceCollection services, ResiliencePolicyOptions options)
private static IAsyncPolicy<HttpResponseMessage> CreateResiliencePolicy(ILogger logger, ResiliencePolicyOptions options)
{
if (options.DisableResilience)
{
return Policy.NoOpAsync<HttpResponseMessage>();
}

var serviceProvider = services.BuildServiceProvider();
var logger = serviceProvider.GetRequiredService<ILogger<T>>();

return HttpPolicyExtensions
.HandleTransientHttpError()
.Or<TimeoutException>()
Expand All @@ -186,45 +196,31 @@ private static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy<T>(IServiceColle
onRetry: (exception, timeSpan, retryCount, context) =>
{
logger.LogWarning(exception.Exception,
"Retry {RetryCount} for {Service} after {Seconds} seconds due to: {Message}",
"Retry {RetryCount} after {Seconds} seconds due to: {Message}",
retryCount,
typeof(T).Name,
timeSpan.TotalSeconds,
exception.Exception.Message);
});
})
.WrapAsync(HttpPolicyExtensions
.HandleTransientHttpError()
.CircuitBreakerAsync(options.CircuitBreakerThreshold, TimeSpan.FromSeconds(options.CircuitBreakerDurationSeconds),
(exception, duration) =>
{
logger.LogError(exception.Exception,
"Circuit breaker opened for {Seconds} seconds due to: {Message}",
duration.TotalSeconds,
exception.Exception.Message);
},
() => logger.LogInformation("Circuit breaker reset")));
}

[ExcludeFromCodeCoverage]
private static IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy<T>(IServiceCollection services, ResiliencePolicyOptions options)
private static void ConfigurePrimaryHttpMessageHandler(HttpMessageHandler messageHandler, IServiceProvider serviceProvider)
{
if (options.DisableResilience)
if (messageHandler is HttpClientHandler clientHandler)
{
return Policy.NoOpAsync<HttpResponseMessage>();
clientHandler.UseCookies = false;
}

var serviceProvider = services.BuildServiceProvider();
var logger = serviceProvider.GetRequiredService<ILogger<T>>();

return HttpPolicyExtensions
.HandleTransientHttpError()
.CircuitBreakerAsync(
handledEventsAllowedBeforeBreaking: options.CircuitBreakerThreshold,
durationOfBreak: TimeSpan.FromSeconds(options.CircuitBreakerDurationSeconds),
onBreak: (exception, duration) =>
{
logger.LogError(
exception.Exception,
"Circuit breaker for {Service} opened for {Seconds} seconds due to: {Message}",
typeof(T).Name,
duration.TotalSeconds,
exception.Exception.Message);
},
onReset: () =>
{
logger.LogInformation(
"Circuit breaker for {Service} reset",
typeof(T).Name);
});
}

private static RefitSettings AddSerializationConfigToRefitSettings(RefitSettings? refitSettings)
Expand Down Expand Up @@ -256,4 +252,7 @@ private static string BuildUserAgent()

return $"PinguAppsAppwriteDotNetClientSdk/{Constants.Version} (.NET/{dotnetVersion}; {RuntimeInformation.OSDescription.Trim()})";
}

// Small helper class to avoid logger type conflicts
internal class ResiliencePolicy { }
}
Loading

0 comments on commit bd20487

Please sign in to comment.