Skip to content
Merged
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
11 changes: 9 additions & 2 deletions src/EdjCase.JsonRpc.Router/BuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ public static IServiceCollection AddJsonRpc(this IServiceCollection serviceColle
/// <param name="serviceCollection">IoC serivce container to register JsonRpc dependencies</param>
/// <param name="configuration">(Optional) Server wide rpc configuration</param>
/// <returns>IoC service container</returns>
public static IServiceCollection AddJsonRpc(this IServiceCollection serviceCollection, RpcServerConfiguration? configuration = null)
public static IServiceCollection AddJsonRpc(
this IServiceCollection serviceCollection,
RpcServerConfiguration? configuration = null
)
{
if (serviceCollection == null)
{
Expand All @@ -62,7 +65,11 @@ public static IServiceCollection AddJsonRpc(this IServiceCollection serviceColle
serviceCollection
.TryAddScoped<IRpcResponseSerializer, DefaultRpcResponseSerializer>();
serviceCollection
.TryAddScoped<IRpcRequestMatcher, DefaultRequestMatcher>();
.TryAddScoped<IRpcRequestMatcher, DefaultRequestMatcher>();
serviceCollection
.TryAddSingleton<RequestMatcherCache>();
serviceCollection
.AddOptions<RequestCacheOptions>();
serviceCollection
.TryAddScoped<IRpcContextAccessor, DefaultContextAccessor>();
serviceCollection
Expand Down
220 changes: 138 additions & 82 deletions src/EdjCase.JsonRpc.Router/Defaults/DefaultRequestMatcher.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
using System;
using System.Buffers;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using EdjCase.JsonRpc.Router;
using EdjCase.JsonRpc.Common;
using EdjCase.JsonRpc.Router.Abstractions;
using EdjCase.JsonRpc.Router.Utilities;
Expand All @@ -13,92 +11,74 @@
using Microsoft.Extensions.Caching.Memory;

namespace EdjCase.JsonRpc.Router.Defaults
{
{

internal class DefaultRequestMatcher : IRpcRequestMatcher
{
private static ConcurrentDictionary<string, ConcurrentDictionary<RpcRequestSignature, IRpcMethodInfo[]>> requestToMethodCache { get; }
= new ConcurrentDictionary<string, ConcurrentDictionary<RpcRequestSignature, IRpcMethodInfo[]>>();

private ILogger<DefaultRequestMatcher> logger { get; }
private IRpcMethodProvider methodProvider { get; }
private IRpcContextAccessor contextAccessor { get; }
private IRpcParameterConverter rpcParameterConverter { get; }

public DefaultRequestMatcher(ILogger<DefaultRequestMatcher> logger,
IRpcMethodProvider methodProvider,
IRpcContextAccessor contextAccessor,
IRpcParameterConverter rpcParameterConverter)
{
this.contextAccessor = contextAccessor;
this.logger = logger;
this.methodProvider = methodProvider;
this.contextAccessor = contextAccessor;
this.rpcParameterConverter = rpcParameterConverter;
}

public IRpcMethodInfo GetMatchingMethod(RpcRequestSignature requestSignature)
{
this.logger.AttemptingToMatchMethod(new string(requestSignature.GetMethodName().Span));

RpcContext context = this.contextAccessor.Get();
IReadOnlyList<IRpcMethodInfo>? methods = this.methodProvider.GetByPath(context.Path);
if (methods == null || !methods.Any())
{
throw new RpcException(RpcErrorCode.MethodNotFound, $"No methods found for route");
}

Span<IRpcMethodInfo> matches = this.FilterAndBuildMethodInfoByRequest(context, methods, requestSignature);
if (matches.Length == 1)
{
this.logger.RequestMatchedMethod();
return matches[0];
}

string errorMessage;
if (matches.Length > 1)
{
var methodInfoList = new List<string>();
foreach (IRpcMethodInfo matchedMethod in matches)
{
var parameterTypeList = new List<string>();
foreach (IRpcParameterInfo parameterInfo in matchedMethod.Parameters)
{
RpcParameterType type = this.rpcParameterConverter.GetRpcParameterType(parameterInfo.RawType);
string parameterType = parameterInfo.Name + ": " + type;
if (parameterInfo.IsOptional)
{
parameterType += "(Optional)";
}
parameterTypeList.Add(parameterType);
}
string parameterString = string.Join(", ", parameterTypeList);
methodInfoList.Add($"{{Name: '{matchedMethod.Name}', Parameters: [{parameterString}]}}");
}
errorMessage = "More than one method matched the rpc request. Unable to invoke due to ambiguity. Methods that matched the same name: " + string.Join(", ", methodInfoList);
}
else
{
//Log diagnostics
this.logger.MethodsInRoute(methods);
errorMessage = "No methods matched request.";
}
throw new RpcException(RpcErrorCode.MethodNotFound, errorMessage);
private IRpcParameterConverter rpcParameterConverter { get; }
private RequestMatcherCache requestMatcherCache { get; }
public DefaultRequestMatcher(
ILogger<DefaultRequestMatcher> logger,
IRpcMethodProvider methodProvider,
IRpcContextAccessor contextAccessor,
IRpcParameterConverter rpcParameterConverter,
RequestMatcherCache requestMatcherCache
)
{
this.contextAccessor = contextAccessor;
this.logger = logger;
this.methodProvider = methodProvider;
this.rpcParameterConverter = rpcParameterConverter;
this.requestMatcherCache = requestMatcherCache;
}


public IRpcMethodInfo GetMatchingMethod(RpcRequestSignature requestSignature)
{
this.logger.AttemptingToMatchMethod(new string(requestSignature.GetMethodName().Span));

// Create efficient cache key from path and signature
string path = this.contextAccessor.Get().Path?.ToString() ?? string.Empty;

IRpcMethodInfo[] matchingMethods = this.requestMatcherCache.GetOrAdd(
path,
requestSignature,
() =>
{
RpcContext context = this.contextAccessor.Get();
IReadOnlyList<IRpcMethodInfo>? methods = this.methodProvider.GetByPath(context.Path);
if (methods == null || !methods.Any())
{
return Array.Empty<IRpcMethodInfo>();
}

return this.GetMatchingMethods(requestSignature, methods);
});


return this.HandleMatchResult(matchingMethods);
}

private IRpcMethodInfo HandleMatchResult(IRpcMethodInfo[] matches)
{
if (matches.Length == 1)
{
this.logger.RequestMatchedMethod();
return matches[0];
}
if (matches.Length > 1)
{
// Format the methods for error message
string errorMessage = "More than one method matched the rpc request. Unable to invoke due to ambiguity.";
throw new RpcException(RpcErrorCode.MethodNotFound, errorMessage);
}

throw new RpcException(RpcErrorCode.MethodNotFound, "No methods matched request.");
}

private IRpcMethodInfo[] FilterAndBuildMethodInfoByRequest(RpcContext context, IReadOnlyList<IRpcMethodInfo> methods, RpcRequestSignature requestSignature)
{
//If the request signature is found, it means we have the methods cached already

string rpcPath = context.Path?.ToString() ?? string.Empty;
var rpcPathMethodsCache = DefaultRequestMatcher.requestToMethodCache.GetOrAdd(rpcPath, path => new ConcurrentDictionary<RpcRequestSignature, IRpcMethodInfo[]>());
return rpcPathMethodsCache.GetOrAdd(requestSignature, BuildMethodCache);
IRpcMethodInfo[] BuildMethodCache(RpcRequestSignature s)
{
return this.GetMatchingMethods(s, methods);
}
}


private IRpcMethodInfo[] GetMatchingMethods(RpcRequestSignature requestSignature, IReadOnlyList<IRpcMethodInfo> methods)
{
IRpcMethodInfo[] methodsWithSameName = ArrayPool<IRpcMethodInfo>.Shared.Rent(methods.Count);
Expand Down Expand Up @@ -260,7 +240,83 @@ private bool ParametersMatch(RpcRequestSignature requestSignature, IReadOnlyList

}

}
}


public class RequestCacheOptions
{
public long SizeLimit { get; set; } = 100;
public TimeSpan? SlidingExpiration { get; set; } = null;
public TimeSpan? AbsoluteExpiration { get; set; } = null;
}

internal class RequestMatcherCache
{
private RequestCacheOptions options { get; }
private MemoryCache memoryCache { get; }

public RequestMatcherCache(IOptions<RequestCacheOptions> options)
{
this.options = options.Value;
this.memoryCache = new MemoryCache(Options.Create(new MemoryCacheOptions
{
SizeLimit = this.options.SizeLimit
}));
}

public IRpcMethodInfo[] GetOrAdd(
string path,
RpcRequestSignature requestSignature,
Func<IRpcMethodInfo[]> resolveFunc
)
{
var cacheKey = new RequestCacheKey(path, requestSignature);

if (this.memoryCache.TryGetValue(cacheKey, out IRpcMethodInfo[]? result))
{
return result!;
}

IRpcMethodInfo[] matchingMethods = resolveFunc();

if (matchingMethods.Length != 1)
{
return matchingMethods; // Don't cache bad matches
}

// Cache with configurable options
var cacheEntryOptions = new MemoryCacheEntryOptions()
.SetSize(1);
if (this.options.SlidingExpiration != null)
{
cacheEntryOptions = cacheEntryOptions.SetSlidingExpiration(this.options.SlidingExpiration.Value);
}
if (this.options.AbsoluteExpiration != null)
{
cacheEntryOptions = cacheEntryOptions.SetAbsoluteExpiration(this.options.AbsoluteExpiration.Value);
}
return this.memoryCache.Set(cacheKey, matchingMethods, cacheEntryOptions);
}
}

internal readonly struct RequestCacheKey : IEquatable<RequestCacheKey>
{
private readonly string path;
private readonly string signatureKey;

public RequestCacheKey(string path, RpcRequestSignature signature)
{
this.path = path;
this.signatureKey = signature.AsString();
}

public bool Equals(RequestCacheKey other) => this.path == other.path && this.signatureKey == other.signatureKey;

public override bool Equals(object? obj) => obj is RequestCacheKey other && this.Equals(other);

public override int GetHashCode() => HashCode.Combine(this.path, this.signatureKey);
}


internal class RpcEndpointInfo
{
Expand Down
8 changes: 7 additions & 1 deletion test/Benchmarks/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,13 @@ public void GlobalSetup()
JsonSerializerSettings = null
});
var rpcParameterConverter = new DefaultRpcParameterConverter(options, new FakeLogger<DefaultRpcParameterConverter>());
this.requestMatcher = new DefaultRequestMatcher(logger, methodProvider, fakeRpcContextAccessor, rpcParameterConverter);
this.requestMatcher = new DefaultRequestMatcher(
logger,
methodProvider,
fakeRpcContextAccessor,
rpcParameterConverter,
new RequestMatcherCache(Options.Create(new RequestCacheOptions()))
);
}

private RpcRequestSignature? requestsignature;
Expand Down
8 changes: 7 additions & 1 deletion test/EdjCase.JsonRpc.Router.Sample/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,13 @@ void ConfigureRpc(RpcServerConfiguration config)
services
.AddControllers()
.Services
.AddJsonRpcWithSwagger(ConfigureRpc, globalJsonSerializerOptions);
.AddJsonRpcWithSwagger(ConfigureRpc, globalJsonSerializerOptions)
.Configure<RequestCacheOptions>(options =>
{
options.SizeLimit = 10;
options.SlidingExpiration = TimeSpan.FromMinutes(5);
options.AbsoluteExpiration = TimeSpan.FromMinutes(90);
});
}


Expand Down
36 changes: 21 additions & 15 deletions test/EdjCase.JsonRpc.Router.Tests/MethodMatcherTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,23 @@ public class MethodMatcherTests
private readonly StaticRpcMethodDataAccessor methodDataAccessor;

public MethodMatcherTests()
{
{
var baserouteData = typeof(MethodMatcherThreeController)
.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance)
.Select(DefaultRpcMethodInfo.FromMethodInfo)
.ToList();

var routeData = new Dictionary<RpcPath, IReadOnlyList<IRpcMethodInfo>>
{
.ToList();
var routeData = new Dictionary<RpcPath, IReadOnlyList<IRpcMethodInfo>>
{
[nameof(MethodMatcherController)] = typeof(MethodMatcherController)
.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance)
.Select(DefaultRpcMethodInfo.FromMethodInfo)
.ToList(),
.ToList(),
[nameof(MethodMatcherDuplicatesController)] = typeof(MethodMatcherDuplicatesController)
.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance)
.Select(DefaultRpcMethodInfo.FromMethodInfo)
.ToList()
};
.ToList()
};
this.methodDataAccessor = new StaticRpcMethodDataAccessor() { Value = new RpcRouteMetaData(baserouteData, routeData)};
}

Expand All @@ -64,7 +64,13 @@ private DefaultRequestMatcher GetMatcher(RpcPath? path = null)


var methodProvider = new StaticRpcMethodProvider(this.methodDataAccessor);
return new DefaultRequestMatcher(logger.Object, methodProvider, rpcContextAccessor.Object, rpcParameterConverter);
return new DefaultRequestMatcher(
logger.Object,
methodProvider,
rpcContextAccessor.Object,
rpcParameterConverter,
new RequestMatcherCache(Options.Create(new RequestCacheOptions()))
);
}

[Fact]
Expand All @@ -81,8 +87,8 @@ public void GetMatchingMethod_WithRpcRoute()
DefaultRequestMatcher path2Matcher = this.GetMatcher(path: typeof(MethodMatcherDuplicatesController).GetTypeInfo().Name);
IRpcMethodInfo path2Match = path2Matcher.GetMatchingMethod(requestSignature);
Assert.NotNull(path2Match);
Assert.NotSame(path1Match, path2Match);

Assert.NotSame(path1Match, path2Match);
DefaultRequestMatcher path3Matcher = this.GetMatcher(path: null);
IRpcMethodInfo path3Match = path3Matcher.GetMatchingMethod(requestSignature);
Assert.NotNull(path2Match);
Expand Down Expand Up @@ -352,15 +358,15 @@ public void GetMatchingMethod__Dictionary_Request_With_Optional_Parameters__Matc

Assert.NotNull(methodInfo);
Assert.Equal(methodName, methodInfo.Name);
}


}
[Fact]
public void GetMatchingMethod_WithoutRpcRoute()
{
string methodName = nameof(MethodMatcherController.GuidTypeMethod);
RpcRequestSignature requestSignature = RpcRequestSignature.Create(methodName, new[] { RpcParameterType.String });

DefaultRequestMatcher path3Matcher = this.GetMatcher(path: null);
IRpcMethodInfo path3Match = path3Matcher.GetMatchingMethod(requestSignature);
Assert.NotNull(path3Match);
Expand Down