-
Notifications
You must be signed in to change notification settings - Fork 446
Conversation
@mikaelm12, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some nits
@@ -491,7 +491,6 @@ public void HubsCannotHaveOverloadedMethods() | |||
Task secondEndPointTask = endPoint.OnConnectedAsync(secondClient.Connection); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
while you are at it: rename the test class and the file to HubEndPointTests
namespace Microsoft.AspNetCore.SignalR | ||
{ | ||
public interface IHubClients | ||
{ | ||
IClientProxy All { get; } | ||
|
||
IClientProxy AllExcept(List<string> excludedIds); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think having params
on a public interface API is a great plan actually... I'd rather use IEnumerable
or IReadOnlyList
and have a params
variant implemented in an extension method. params
forces you to allocate an array, if you don't already have one (until we get params IEnumerable
someday...)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought about IEnumerable
but there is always a problem with lazy evaluation. IReadOnlyList
is a good option. Agree that params
is a little weird (you don't know connectionIds upfront but you need to know the count) but it saves you a few keystrokes if you want to exclude just one connectionId you store in a variable (e.g. .Others
).
@@ -3,6 +3,7 @@ | |||
|
|||
using System; | |||
using System.Threading.Tasks; | |||
using System.Collections.Generic; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Edit: Yeah it is, I was looking at the wrong file when I originally commented.
@@ -141,6 +142,11 @@ public override Task InvokeAllAsync(string methodName, object[] args) | |||
return _wrappedHubLifetimeManager.InvokeAllAsync(methodName, args); | |||
} | |||
|
|||
public override Task InvokeAllExceptAsync(List<string> excludedIds, string methodName, object[] args) | |||
{ | |||
return _wrappedHubLifetimeManager.InvokeAllExceptAsync( excludedIds, methodName, args); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
format
{ | ||
return InvokeAllWhere(methodName, args, connection => | ||
{ | ||
return !excludedIds.Contains(connection.ConnectionId); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if this is for now but this feels slow
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, using a HashSet<string>
for excludedIds
would be faster, since it has a O(1) contains test
For redis; servers can subscribe to a new channel Or we could create an internal |
{ | ||
var serviceProvider = CreateServiceProvider(); | ||
|
||
dynamic endPoint = serviceProvider.GetService(GetEndPointType(typeof(MethodHub))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
var endPoint = serviceProvider.GetService<HubEndPoint<MethodHub>>();
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dynamic not needed in this test
Great to see this feature coming. I think its implementation should be the reverse of I also like the idea of a Can you change the type of |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interface all the things!
{ | ||
return InvokeAllWhere(methodName, args, connection => | ||
{ | ||
return !excludedIds.Contains(connection.ConnectionId); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, using a HashSet<string>
for excludedIds
would be faster, since it has a O(1) contains test
@@ -340,6 +339,11 @@ private HubMessage DeserializeMessage(RedisValue data) | |||
return message; | |||
} | |||
|
|||
public override Task InvokeAllExceptAsync(List<string> excludedIds, string methodName, object[] args) | |||
{ | |||
throw new NotImplementedException(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Assuming this is part of the WIP-ness of this PR ;)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah lol. Working this out now 😄
@@ -20,6 +22,11 @@ public HubContext(HubLifetimeManager<THub> lifetimeManager) | |||
|
|||
public virtual IGroupManager Groups { get; } | |||
|
|||
public IClientProxy AllExcept(List<string> excludedIds) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should use IReadOnlyList<string>
or IEnumerable<string>
here. We never plan to write to this list, and we don't care exactly how it's implemented. In fact, if our implementations are going to use HashSet<string>
, IEnumerable
would be better since we're going to need to convert it to a different type of collection anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, should have a params string[] excludedIds
overload (possibly via an extension method) so you can call .AllExcept(connection1, connection2, connection3)...
@@ -13,6 +14,8 @@ public abstract class HubLifetimeManager<THub> | |||
|
|||
public abstract Task InvokeAllAsync(string methodName, object[] args); | |||
|
|||
public abstract Task InvokeAllExceptAsync(List<string> excludedIds, string methodName, object[] args); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IEnumerable<string>
/IReadOnlyList<string>
namespace Microsoft.AspNetCore.SignalR | ||
{ | ||
public interface IHubClients | ||
{ | ||
IClientProxy All { get; } | ||
|
||
IClientProxy AllExcept(List<string> excludedIds); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think having params
on a public interface API is a great plan actually... I'd rather use IEnumerable
or IReadOnlyList
and have a params
variant implemented in an extension method. params
forces you to allocate an array, if you don't already have one (until we get params IEnumerable
someday...)
private readonly HubLifetimeManager<THub> _lifetimeManager; | ||
private readonly List<string> _excludedIds; | ||
|
||
public AllClientsExceptProxy(HubLifetimeManager<THub> lifetimeManager, List<string> excludedIds) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IEnumerable<string>
/IReadOnlyList<string>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not ICollection<string>
? It is the lowest level interface that offers Contains()
.
Edit: The dev can choose between convenience (List<string>
, or params string[]
) and performance (Set<string>
) or even some other custom implementation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We never need to write to the collection, so we should avoid using a writable interface, hence IReadOnlyCollection
or IReadOnlyList
would be a better option.
In general, we want to lead users towards good performance without them having to know how our code is implemented, so we try to avoid leading users to a path where they have to consider the performance of collection objects they give us. It's our job to make processing this collection fast, not the user's.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IReadOnlyCollection
and IReadOnlyList
do not have a Contains()
method, which is essential to the implementation.
And if we care about performance, we must allow passing an ISet
-based collection.
So, I think it is better to take a (potentially) writable interface that performs well than take a read-only collection that SignalR must first convert to something like HashSet
.
That interface ends up being ICollection
because we also want to allow IList
for convenience. Otherwise, the overload method taking params string[]
cannot pass its parameter directly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The point is moot until we have benchmarks to properly understand the performance characteristics in practical scenarios. Given that the most common scenario we've seen for this involves a very small number of entries in the list, we may just spin our wheels here for a while with almost no gain :).
The original comment is just about using an interface over the concrete List<T>
type currently used. IReadOnlyCollection<T>
/IReadOnlyList<T>
are the "correct" expressions of what this public API requires, which is generally our first stop when building an API. Performance decisions to compromise that have to be data-driven and based on actual usage.
Tested this functionality with the presence chat sample and it looks pretty good 😄 |
233374a
to
2b7b869
Compare
⬆️ 📅 |
{ | ||
return InvokeAllWhere(methodName, args, connection => | ||
{ | ||
var excludedIdsSet = new HashSet<string>(excludedIds); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move this outside of the callback, right now you allocate a HashSet for every connection
{ | ||
var serviceProvider = CreateServiceProvider(); | ||
|
||
dynamic endPoint = serviceProvider.GetService(GetEndPointType(typeof(MethodHub))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dynamic not needed in this test
@@ -20,6 +22,11 @@ public HubContext(HubLifetimeManager<THub> lifetimeManager) | |||
|
|||
public virtual IGroupManager Groups { get; } | |||
|
|||
public IClientProxy AllExcept(IReadOnlyList<string> excludedIds) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some public APIs use IReadOnlyList
some use IReadOnlyCollection
. Normalize them
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should all be IReadOnlyList
now
namespace Microsoft.AspNetCore.SignalR | ||
{ | ||
public interface IHubClients | ||
{ | ||
IClientProxy All { get; } | ||
|
||
IClientProxy AllExcept(IReadOnlyList<string> excludedIds); | ||
//IClientProxy AllExcept(params string[] excludedIds); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove
@@ -124,8 +124,9 @@ public Task<string> SendInvocationAsync(string methodName, params object[] args) | |||
public async Task<string> SendInvocationAsync(string methodName, bool nonBlocking, params object[] args) | |||
{ | |||
var invocationId = GetInvocationId(); | |||
|
|||
var payload = _protocolReaderWriter.WriteMessage(new InvocationMessage(invocationId, nonBlocking, methodName, args)); | |||
var invocationMessage = new InvocationMessage(invocationId, nonBlocking, methodName, args); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Revert
public override Task InvokeAllExceptAsync(string methodName, object[] args, IReadOnlyCollection<string> excludedIds) | ||
{ | ||
var message = new InvocationMessage(GetInvocationId(), nonBlocking: true, target: methodName, arguments: args); | ||
var data = new object[] { message, excludedIds }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks... What was wrong with making a class that wraps InvocationMessage and adds a IReadOnlyCollection?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was a WIP hack that I didn't update 😄
🆙 📅 |
var message = DeserializeMessage<RedisExcludeClientMessage>(data); | ||
var invocationMessage = (InvocationMessage)message.HubMessage; | ||
var excludedIds = message.ExcludedIds; | ||
var excludedIdsSet = new HashSet<string>(excludedIds); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't used
} | ||
|
||
private async Task PublishAsync(string channel, HubMessage hubMessage) | ||
private async Task PublishAsync(string channel, object args) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Revert
@@ -362,6 +394,17 @@ public override void WriteLine(string value) | |||
} | |||
} | |||
|
|||
private class RedisExcludeClientMessage |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be able to inherit from InvocationMessage
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it make more sense to inherit from HubMessage?
{ | ||
return InvokeAllWhere(methodName, args, connection => | ||
{ | ||
var excludedIdsSet = new HashSet<string>(excludedIds); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should just get rid of this and call .Contains
on excludedIds
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, this will create a new HashSet for every check. If we decide we need a HashSet for optimization, we would want to initialize it outside the lambda so there's only one instance.
public override Task InvokeAllExceptAsync(string methodName, object[] args, IReadOnlyList<string> excludedIds) | ||
{ | ||
var message = new InvocationMessage(GetInvocationId(), nonBlocking: true, target: methodName, arguments: args); | ||
var redisExcludeMessage = new RedisExcludeClientMessage(message, excludedIds); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The point of making it inherit from InvocationMessage
was that you could avoid newing up two objects, and just do a single RedisExcludeClientMessage
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hrm... feels like a a premature optimization, but I guess we can do it.
Assert.Equal("Send", invocation.Target); | ||
Assert.Equal("To second", invocation.Arguments[0]); | ||
|
||
var thirdClientResult = await thirdClient.ReadAsync().OrTimeout(); ; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
extra semi-colon
{ | ||
public IReadOnlyList<string> ExcludedIds; | ||
|
||
public RedisExcludeClientsMessage(string invocationId, bool nonBlocking, string target, IReadOnlyList<string> excludedIds, params object[] arguments) : base(invocationId, nonBlocking, target, arguments) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should probably add a newline
|
||
public override Task InvokeAllExceptAsync(string methodName, object[] args, IReadOnlyList<string> excludedIds) | ||
{ | ||
var message = new RedisExcludeClientsMessage(GetInvocationId(), nonBlocking: true, target: methodName, excludedIds: excludedIds ,arguments: args); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
formatting excludedIds ,arguments:
var invocationMessage = (InvocationMessage)message.HubMessage; | ||
var excludedIds = message.ExcludedIds; | ||
|
||
//// TODO: This isn't going to work when we allow JsonSerializer customization or add Protobuf |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How is it not going to work? If we're just writing serialized data directly to the connection, then we're broken for MsgPack too, if one node is using JSON and the other is using MsgPack. If we're serializing separately for every connection, then it should work fine with JsonSerializer customization and Protobuf because each send uses the client's configured serializers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I derped and commented on outdated stuff. Disregard
public override Task InvokeAllExceptAsync(string methodName, object[] args, IReadOnlyList<string> excludedIds) | ||
{ | ||
var message = new InvocationMessage(GetInvocationId(), nonBlocking: true, target: methodName, arguments: args); | ||
var redisExcludeMessage = new RedisExcludeClientMessage(message, excludedIds); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hrm... feels like a a premature optimization, but I guess we can do it.
HubMessage = message; | ||
ExcludedIds = ids; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
need an extra line
{ | ||
return InvokeAllWhere(methodName, args, connection => | ||
{ | ||
var excludedIdsSet = new HashSet<string>(excludedIds); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, this will create a new HashSet for every check. If we decide we need a HashSet for optimization, we would want to initialize it outside the lambda so there's only one instance.
var message = DeserializeMessage<RedisExcludeClientsMessage>(data); | ||
var excludedIds = message.ExcludedIds; | ||
|
||
//// TODO: This isn't going to work when we allow JsonSerializer customization or add Protobuf |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you clarify how this is broken with JsonSerializer customization and Protobuf? I'd think it'd already be broken for MsgPack if that was the case. We should be re-serializing for each client in the current implementation, right? It's slower, but guarantees it's correct and should work fine with serializer customization.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment is a copy pasta result. Not sure tbh
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
super nit: the comment has too many ////
_logger.LogInformation("Subscribing to channel: {channel}", channelName); | ||
_bus.Subscribe(channelName, async (c, data) => | ||
{ | ||
await previousBroadcastTask; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this correct to use the same task as broadcast?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh yeah. This doesn't look good.
} | ||
} | ||
|
||
previousBroadcastTask = Task.WhenAll(tasks); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is wrong/racey - you can replace a task that has not been finished with another task that is not finished. Sharing the same task between broadcast and .allexcept makes it even more racey
If there's no further discussion I'm gonna merge this eod |
Wanted some initial review + wanted to get some discussion on how to do this for the RedisHubLifetimeManager. Discussed this with @moozzyk yesterday a little but I figured it would be good to put it out here.