diff --git a/README.md b/README.md index 1f08f0b..5473c34 100644 --- a/README.md +++ b/README.md @@ -595,7 +595,8 @@ In case you need consistent cache across clusters or data centers you can use ca "Interval": "00:01:00", "MaxErrors": 50, "SwitchOffTime": "00:02:00" - } + }, + "SyncEndpointsAuthAllowAnonymous": false } } } @@ -626,6 +627,8 @@ app.UseEndpoints(endpoints => `Configuration` argument here is a property on a `Startup` instance +If you have implicit authorization configured for your service you can allow anonymous access to sync endpoints by setting `MemcachedConfiguration.SyncSettings.SyncEndpointsAuthAllowAnonymous` to `true`. + When using cache synchronization feature, the `MemcachedClientResult.SyncSuccess` property can be inspected to determine whether the sync operation succeeded. When cache synchronization is not used this property is set to `false`. To check whether the cache synchronization is configured and enabled call the `IMemcachedClient.IsCacheSyncEnabled` method. diff --git a/src/Aer.Memcached.Client/Config/MemcachedConfiguration.cs b/src/Aer.Memcached.Client/Config/MemcachedConfiguration.cs index 5c58fc7..57026ce 100644 --- a/src/Aer.Memcached.Client/Config/MemcachedConfiguration.cs +++ b/src/Aer.Memcached.Client/Config/MemcachedConfiguration.cs @@ -341,6 +341,11 @@ public class SynchronizationSettings /// Settings of circuit breaker. /// public CacheSyncCircuitBreakerSettings CacheSyncCircuitBreaker { get; set; } + + /// + /// If set to true allows anonymous access to cache sync endpoints. + /// + public bool SyncEndpointsAuthAllowAnonymous { get; set; } } public class SyncServer diff --git a/src/Aer.Memcached.Client/ConnectionPool/PooledSocket.cs b/src/Aer.Memcached.Client/ConnectionPool/PooledSocket.cs index 25e44b5..131076a 100644 --- a/src/Aer.Memcached.Client/ConnectionPool/PooledSocket.cs +++ b/src/Aer.Memcached.Client/ConnectionPool/PooledSocket.cs @@ -202,7 +202,7 @@ public async Task WriteAsync(IList> buffers) /// /// Releases all resources used by this instance and shuts down the inner . This instance will not be usable anymore. /// - /// Use the method if you want to release this instance back into the pool. + /// Use the method if you want to release this instance back into the pool. public void Destroy() { Dispose(true); diff --git a/src/Aer.Memcached.Client/Models/MemcachedClientValueResult.cs b/src/Aer.Memcached.Client/Models/MemcachedClientValueResult.cs index bcf0dca..a2c3bd2 100644 --- a/src/Aer.Memcached.Client/Models/MemcachedClientValueResult.cs +++ b/src/Aer.Memcached.Client/Models/MemcachedClientValueResult.cs @@ -83,6 +83,7 @@ internal static MemcachedClientValueResult Cancelled(string operationName) /// /// Creates an instance of that indicates request cancellation. /// + /// The memcached operation that was cancelled. /// The default value for property. internal static MemcachedClientValueResult Cancelled(string operationName, T defaultResultValue) => new( diff --git a/src/Aer.Memcached/Abstractions/INodeHealthChecker.cs b/src/Aer.Memcached/Abstractions/INodeHealthChecker.cs index 6efad56..006c078 100644 --- a/src/Aer.Memcached/Abstractions/INodeHealthChecker.cs +++ b/src/Aer.Memcached/Abstractions/INodeHealthChecker.cs @@ -2,7 +2,11 @@ namespace Aer.Memcached.Abstractions; -public interface INodeHealthChecker where TNode: class, INode +internal interface INodeHealthChecker where TNode: class, INode { + /// + /// Checks if the given node is dead. + /// + /// The node to check. Task CheckNodeIsDeadAsync(TNode node); } \ No newline at end of file diff --git a/src/Aer.Memcached/Abstractions/INodeProvider.cs b/src/Aer.Memcached/Abstractions/INodeProvider.cs index e3e3768..5caab18 100644 --- a/src/Aer.Memcached/Abstractions/INodeProvider.cs +++ b/src/Aer.Memcached/Abstractions/INodeProvider.cs @@ -2,9 +2,19 @@ namespace Aer.Memcached.Abstractions; -public interface INodeProvider where TNode : class, INode +/// +/// Provides a collection of nodes for the consistent hashing algorithm. +/// +/// The type of the node. +internal interface INodeProvider where TNode : class, INode { + /// + /// Indicates whether the node provider is properly configured and ready to provide nodes. + /// bool IsConfigured(); + /// + /// Gets the collection of nodes. + /// ICollection GetNodes(); } \ No newline at end of file diff --git a/src/Aer.Memcached/Aer.Memcached.csproj b/src/Aer.Memcached/Aer.Memcached.csproj index abdf97c..30d9e30 100644 --- a/src/Aer.Memcached/Aer.Memcached.csproj +++ b/src/Aer.Memcached/Aer.Memcached.csproj @@ -41,6 +41,9 @@ <_Parameter1>$(MSBuildProjectName).Tests + + <_Parameter1>DynamicProxyGenAssembly2 + diff --git a/src/Aer.Memcached/Diagnostics/Configuration/MetricsProviderType.cs b/src/Aer.Memcached/Diagnostics/Configuration/MetricsProviderType.cs index 92b50f9..ac5e306 100644 --- a/src/Aer.Memcached/Diagnostics/Configuration/MetricsProviderType.cs +++ b/src/Aer.Memcached/Diagnostics/Configuration/MetricsProviderType.cs @@ -2,6 +2,6 @@ internal enum MetricsProviderType { - Prometheus, - OpenTelemetry + Prometheus, + OpenTelemetry } diff --git a/src/Aer.Memcached/Diagnostics/Listeners/LoggingMemcachedDiagnosticListener.cs b/src/Aer.Memcached/Diagnostics/Listeners/LoggingMemcachedDiagnosticListener.cs index 3acaab3..e58e133 100644 --- a/src/Aer.Memcached/Diagnostics/Listeners/LoggingMemcachedDiagnosticListener.cs +++ b/src/Aer.Memcached/Diagnostics/Listeners/LoggingMemcachedDiagnosticListener.cs @@ -6,7 +6,7 @@ namespace Aer.Memcached.Diagnostics.Listeners; -public class LoggingMemcachedDiagnosticListener +internal class LoggingMemcachedDiagnosticListener { private readonly ILogger _logger; private readonly MemcachedConfiguration _config; diff --git a/src/Aer.Memcached/Diagnostics/Listeners/MetricsMemcachedDiagnosticListener.cs b/src/Aer.Memcached/Diagnostics/Listeners/MetricsMemcachedDiagnosticListener.cs index dff6cbd..6c95783 100644 --- a/src/Aer.Memcached/Diagnostics/Listeners/MetricsMemcachedDiagnosticListener.cs +++ b/src/Aer.Memcached/Diagnostics/Listeners/MetricsMemcachedDiagnosticListener.cs @@ -10,7 +10,9 @@ internal class MetricsMemcachedDiagnosticListener private readonly MemcachedMetricsProvider _metricsProvider; private readonly MemcachedConfiguration _config; - public MetricsMemcachedDiagnosticListener(MemcachedMetricsProvider metricsProvider, IOptions config) + public MetricsMemcachedDiagnosticListener( + MemcachedMetricsProvider metricsProvider, + IOptions config) { _metricsProvider = metricsProvider; _config = config.Value; @@ -23,10 +25,10 @@ public void ObserveCommandDuration(string commandName, double duration) { return; } - + _metricsProvider.ObserveCommandDurationSeconds(commandName, duration); } - + [DiagnosticName(MemcachedDiagnosticSource.CommandsTotalDiagnosticName)] public void ObserveCommandsTotal(string commandName, string isSuccessful) { @@ -34,7 +36,7 @@ public void ObserveCommandsTotal(string commandName, string isSuccessful) { return; } - + _metricsProvider.ObserveExecutedCommand(commandName, isSuccessful); } @@ -45,7 +47,7 @@ public void ObserveSocketPoolUsedSocketsCount(string enpointAddress, int usedSoc { return; } - + _metricsProvider.ObserveSocketPoolUsedSocketsCount(enpointAddress, usedSocketCount); } } \ No newline at end of file diff --git a/src/Aer.Memcached/Diagnostics/MemcachedMetricsProvider.cs b/src/Aer.Memcached/Diagnostics/MemcachedMetricsProvider.cs index 7d5321e..df9a708 100644 --- a/src/Aer.Memcached/Diagnostics/MemcachedMetricsProvider.cs +++ b/src/Aer.Memcached/Diagnostics/MemcachedMetricsProvider.cs @@ -26,8 +26,8 @@ internal class MemcachedMetricsProvider public static readonly Dictionary MetricsBuckets = new() { - [CommandDurationSecondsMetricName] = new[] {0.0005, 0.001, 0.005, 0.007, 0.015, 0.05, 0.2, 0.5, 1}, - [SocketPoolUsedSocketsCountsMetricName] = new[] {0, 10.0, 20.0, 50.0, 100.0, 200.0, 500.0} + [CommandDurationSecondsMetricName] = [0.0005, 0.001, 0.005, 0.007, 0.015, 0.05, 0.2, 0.5, 1], + [SocketPoolUsedSocketsCountsMetricName] = [0, 10.0, 20.0, 50.0, 100.0, 200.0, 500.0] }; private const string CommandNameLabel = "command_name"; @@ -79,18 +79,18 @@ public MemcachedMetricsProvider( CommandDurationSecondsMetricName, "", MetricsBuckets[CommandDurationSecondsMetricName], - labelNames: new[] {CommandNameLabel}); + labelNames: [CommandNameLabel]); _socketPoolUsedSocketsCounts = metricFactory.CreateHistogram( SocketPoolUsedSocketsCountsMetricName, "", MetricsBuckets[SocketPoolUsedSocketsCountsMetricName], - labelNames: new[] {SocketPoolEndpointAddressLabel}); + labelNames: [SocketPoolEndpointAddressLabel]); _commandsTotal = metricFactory.CreateCounter( CommandsTotalOtelMetricName, "", - labelNames: new[] {CommandNameLabel, IsSuccessfulLabel}); + labelNames: [CommandNameLabel, IsSuccessfulLabel]); } /// @@ -129,7 +129,7 @@ public void ObserveExecutedCommand(string commandName, string isSuccessful) /// /// Observes specified endpoint socket pool used sockets count. /// - /// The address of an endpoint to obeserve socket pool state for. + /// The address of an endpoint to observe socket pool state for. /// The number of currently used sockets for the specified pool. public void ObserveSocketPoolUsedSocketsCount(string endpointAddress, int usedSocketCount) { diff --git a/src/Aer.Memcached/Helpers/EndpointBuilderExtensions.cs b/src/Aer.Memcached/Helpers/EndpointBuilderExtensions.cs new file mode 100644 index 0000000..f4e52d9 --- /dev/null +++ b/src/Aer.Memcached/Helpers/EndpointBuilderExtensions.cs @@ -0,0 +1,24 @@ +using Aer.Memcached.Client.Config; +using Microsoft.AspNetCore.Builder; + +namespace Aer.Memcached.Helpers; + +internal static class EndpointBuilderExtensions +{ + public static RouteHandlerBuilder AllowAnonymousIfConfigured( + this RouteHandlerBuilder endpointBuilder, + MemcachedConfiguration configuration) + { + if (configuration.SyncSettings == null) + { + return endpointBuilder; + } + + if (configuration.SyncSettings.SyncEndpointsAuthAllowAnonymous) + { + endpointBuilder.AllowAnonymous(); + } + + return endpointBuilder; + } +} diff --git a/src/Aer.Memcached/Pod.cs b/src/Aer.Memcached/Pod.cs index 9d74ad1..98162d9 100644 --- a/src/Aer.Memcached/Pod.cs +++ b/src/Aer.Memcached/Pod.cs @@ -4,14 +4,29 @@ namespace Aer.Memcached; -public class Pod: INode +/// +/// Represents a Memcached pod (node) in the cluster. +/// +public class Pod : INode { private readonly string _nodeKey; + /// + /// The IP address of the Memcached pod. + /// public string IpAddress { get; } - + + /// + /// The port on which the Memcached pod is listening. + /// public int MemcachedPort { get; } + /// + /// Initializes a new instance of the class with the specified IP address and port. + /// + /// The IP address of the Memcached pod. + /// The port on which the Memcached pod is listening. + /// Occurs when either or is not specified or empty. public Pod(string ipAddress, int port = MemcachedConfiguration.DefaultMemcachedPort) { if (ipAddress is null or {Length: 0}) @@ -26,30 +41,38 @@ public Pod(string ipAddress, int port = MemcachedConfiguration.DefaultMemcachedP IpAddress = ipAddress; MemcachedPort = port; - - _nodeKey = $"{IpAddress}:{MemcachedPort}"; + + _nodeKey = $"{IpAddress}:{MemcachedPort}"; } + /// public string GetKey() { return _nodeKey; } + /// public EndPoint GetEndpoint() { return new DnsEndPoint(IpAddress, MemcachedPort); } + /// + /// Determines whether the specified Pod is equal to the current Pod. + /// + /// The pod to compare this pod to. protected bool Equals(Pod other) { return IpAddress == other.IpAddress && MemcachedPort == other.MemcachedPort; } + /// public bool Equals(INode other) { return GetKey() == other?.GetKey(); } + /// public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) @@ -67,15 +90,16 @@ public override bool Equals(object obj) return false; } - return Equals((Pod)obj); + return Equals((Pod) obj); } + /// public override int GetHashCode() { var ipAddressHash = IpAddress.GetHashCode(); var portHash = MemcachedPort.GetHashCode(); - + return HashCode.Combine(ipAddressHash, portHash); } } \ No newline at end of file diff --git a/src/Aer.Memcached/ServiceCollectionExtensions.cs b/src/Aer.Memcached/ServiceCollectionExtensions.cs index 48dd140..a914fdc 100644 --- a/src/Aer.Memcached/ServiceCollectionExtensions.cs +++ b/src/Aer.Memcached/ServiceCollectionExtensions.cs @@ -14,6 +14,7 @@ using Aer.Memcached.Client.Serializers; using Aer.Memcached.Diagnostics; using Aer.Memcached.Diagnostics.Listeners; +using Aer.Memcached.Helpers; using Aer.Memcached.Infrastructure; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; @@ -28,8 +29,16 @@ namespace Aer.Memcached; +/// +/// The extension methods for setting up Memcached services in an . +/// public static class ServiceCollectionExtensions { + /// + /// Adds Memcached services to the with settings from app configuration. + /// + /// The service collection. + /// The application configuration. public static IServiceCollection AddMemcached( this IServiceCollection services, IConfiguration configuration) @@ -108,6 +117,11 @@ private static void AddOpenTelemetryMetrics(this IServiceCollection services, st }); } + /// + /// Enables Memcached diagnostics listeners for metrics and logging. + /// + /// The application builder instance. + /// The application configuration. public static IApplicationBuilder EnableMemcachedDiagnostics( this IApplicationBuilder applicationBuilder, IConfiguration configuration) @@ -132,15 +146,27 @@ public static IApplicationBuilder EnableMemcachedDiagnostics( return applicationBuilder; } - public static void AddMemcachedEndpoints(this IEndpointRouteBuilder endpoints, IConfiguration configuration) + /// + /// Adds endpoints for internode synchronization to the with settings from app configuration. + /// + /// The service endpoints route builder instance. + /// The application configuration. + public static void AddMemcachedEndpoints( + this IEndpointRouteBuilder endpoints, + IConfiguration configuration) { - var config = configuration.GetSection(nameof(MemcachedConfiguration)).Get(); + var config = configuration + .GetSection(nameof(MemcachedConfiguration)) + .Get(); + var deleteEndpoint = config.SyncSettings == null ? MemcachedConfiguration.DefaultDeleteEndpoint : config.SyncSettings.DeleteEndpoint; + var flushEndpoint = config.SyncSettings == null ? MemcachedConfiguration.DefaultFlushEndpoint : config.SyncSettings.FlushEndpoint; + var getEndpoint = config.SyncSettings == null ? MemcachedConfiguration.DefaultGetEndpoint : config.SyncSettings.GetEndpoint; @@ -159,15 +185,17 @@ await memcachedClient.MultiDeleteAsync( { IsManualSyncOn = false }); - }); + } + ).AllowAnonymousIfConfigured(config); endpoints.MapPost( flushEndpoint, async (IMemcachedClient memcachedClient, CancellationToken token) => { await memcachedClient.FlushAsync(token); - }); - + } + ).AllowAnonymousIfConfigured(config); + if (config.SyncSettings != null) { endpoints.MapPost( @@ -183,12 +211,13 @@ await memcachedClient.MultiStoreSynchronizeDataAsync( model.ExpirationTime, token, model.ExpirationMap); - }); + } + ).AllowAnonymousIfConfigured(config); } - + endpoints.MapPost( getEndpoint, - (MultiGetTypedRequest request, IMemcachedClient memcachedClient, CancellationToken token) => + (MultiGetTypedRequest request, IMemcachedClient memcachedClient, CancellationToken token) => { try { @@ -198,30 +227,34 @@ await memcachedClient.MultiStoreSynchronizeDataAsync( return Results.Ok( $"Type is not found. Try {typeof(string).FullName} or {typeof(object).FullName}"); } - + var method = typeof(MemcachedClient).GetMethod(nameof(MemcachedClient.MultiGetAsync)); if (method == null) { return Results.Ok($"Method for the type {resolvedType} is not found"); } - + var genericMethod = method.MakeGenericMethod(resolvedType); - - var task = genericMethod.Invoke(memcachedClient, parameters: [request.Keys, token, null, (uint)0]) as Task; + + var task = + genericMethod.Invoke( + memcachedClient, + parameters: [request.Keys, token, null, (uint) 0]) as Task; if (task == null) { - return Results.Ok($"Method for the type {resolvedType} is not found"); + return Results.Ok($"Method for the type {resolvedType} is not found"); } - + var result = task.GetType().GetProperty("Result")?.GetValue(task); - + return Results.Ok(Newtonsoft.Json.JsonConvert.SerializeObject(result)); } catch (Exception e) { return Results.BadRequest(e); } - }); + } + ).AllowAnonymousIfConfigured(config); } private class MultiGetTypedRequest diff --git a/tests/Aer.Memcached.Tests/Base/MemcachedClientTestsBase.cs b/tests/Aer.Memcached.Tests/Base/MemcachedClientTestsBase.cs index b29237f..d08c50b 100644 --- a/tests/Aer.Memcached.Tests/Base/MemcachedClientTestsBase.cs +++ b/tests/Aer.Memcached.Tests/Base/MemcachedClientTestsBase.cs @@ -24,7 +24,7 @@ public abstract class MemcachedClientTestsBase protected readonly Fixture Fixture; - protected readonly ObjectBinarySerializerType BinarySerizerType; + protected readonly ObjectBinarySerializerType BinarySerializerType; protected readonly ServiceProvider ServiceProvider; @@ -33,7 +33,7 @@ protected MemcachedClientTestsBase( ObjectBinarySerializerType binarySerializerType = ObjectBinarySerializerType.Bson, bool isAllowLongKeys = false) { - BinarySerizerType = binarySerializerType; + BinarySerializerType = binarySerializerType; var hashCalculator = new HashCalculator(); @@ -70,7 +70,10 @@ protected MemcachedClientTestsBase( SocketPoolDiagnosticsLoggingEventLevel = LogLevel.Information }, BinarySerializerType = binarySerializerType, - IsAllowLongKeys = isAllowLongKeys + IsAllowLongKeys = isAllowLongKeys, + SyncSettings = new (){ + SyncEndpointsAuthAllowAnonymous = false + } }; var authProvider = new DefaultAuthenticationProvider( diff --git a/tests/Aer.Memcached.Tests/TestClasses/MemcachedClientMethodsTestsBase.cs b/tests/Aer.Memcached.Tests/TestClasses/MemcachedClientMethodsTestsBase.cs index 2cce562..d588ae2 100644 --- a/tests/Aer.Memcached.Tests/TestClasses/MemcachedClientMethodsTestsBase.cs +++ b/tests/Aer.Memcached.Tests/TestClasses/MemcachedClientMethodsTestsBase.cs @@ -51,7 +51,7 @@ public async Task StoreAndGet_CheckAllTypes() await StoreAndGet_CheckType>(); await StoreAndGet_CheckType(); - if (BinarySerizerType != ObjectBinarySerializerType.Bson) + if (BinarySerializerType != ObjectBinarySerializerType.Bson) { // BSON serializer can't serialize dictionaries with non-primitive objects as keys await StoreAndGet_CheckType>(); @@ -82,7 +82,7 @@ public async Task MultiStoreAndGet_CheckAllTypes(bool withReplicas) await MultiStoreAndGet_CheckType>(withReplicas); await MultiStoreAndGet_CheckType(withReplicas); - if (BinarySerizerType != ObjectBinarySerializerType.Bson) + if (BinarySerializerType != ObjectBinarySerializerType.Bson) { // BSON serializer can't serialize dictionaries with non-primitive objects as keys await StoreAndGet_CheckType>(); @@ -121,7 +121,7 @@ public async Task Get_CheckAllTypes_DefaultValue() await Get_CheckType>(); await Get_CheckType(); - if (BinarySerizerType != ObjectBinarySerializerType.Bson) + if (BinarySerializerType != ObjectBinarySerializerType.Bson) { // BSON serializer can't serialize dictionaries with non-primitive objects as keys await StoreAndGet_CheckType>(); @@ -151,7 +151,7 @@ public async Task MultiGet_CheckAllTypes_EmptyDictionary(bool withReplicas) await MultiGet_CheckType(withReplicas); await MultiGet_CheckType>(withReplicas); - if (BinarySerizerType != ObjectBinarySerializerType.Bson) + if (BinarySerializerType != ObjectBinarySerializerType.Bson) { // BSON serializer can't serialize dictionaries with non-primitive objects as keys await StoreAndGet_CheckType>(); @@ -715,7 +715,7 @@ public async Task StoreAndGet_RecursiveModel() [TestMethod] public async Task StoreAndGet_ObjectWithNested_IgnoreReferenceLoopHandling() { - if (BinarySerizerType != ObjectBinarySerializerType.Bson) + if (BinarySerializerType != ObjectBinarySerializerType.Bson) { // only BSON serializer can ignore reference loops return;