Skip to content
This repository was archived by the owner on Dec 18, 2018. It is now read-only.

Conversation

@analogrelay
Copy link
Contributor

@analogrelay analogrelay commented Nov 27, 2017

This change teaches HubEndpoint to send PingMessage.Instance to the client at regular intervals, if no other messages have been sent. But only for SSE and WebSockets.

I found a sneaky and possibly ingenious (or possibly terrible) way to implement keep-alive. Since the HubEndpoint already creates a Channel that it uses to marshal writes from multiple writers, I just wrap it in a special ChannelReader that emits PingMessage.Instance if WaitToReadAsync blocks for more than the desired interval.

There is a feature, IConnectionInherentKeepAliveFeature, that indicates if a transport has "inherent keep-alive", like Long Polling (for which the poll itself serves as a way for the client to tell if the server is gone)

Notes:

  • package-lock.json got changed, but I sense it will keep changing if we don't check in the changes, they're just ordering changes, no new packages.
  • The Redis Docker tests are flaky on my home desktop (seems to be more than just Flaky Docker Redis tests #1160 after all @BrennanConroy) so I had to disable them

Fixes #1156

Also fixes #715

disconnectButton.disabled = false;
console.log('http://' + document.location.host + '/' + hubRoute);
connection = new signalR.HubConnection(hubRoute, logger, { transport: transportType, logger: logger });
connection = new signalR.HubConnection(hubRoute, { transport: transportType, logging: logger });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mine's better, I fixed all of them :P.

using System.Threading.Channels;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Sockets.Features;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: sort

public LongPollingOptions LongPolling { get; } = new LongPollingOptions();

/// <summary>
/// The interval at which keep-alive messages should be sent. This will be
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exclamation point at the end This will be! :trollface:


namespace Microsoft.AspNetCore.SignalR.Core.Internal
{
internal class KeepAliveChannel : Channel<HubMessage>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not too sure about this..

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The thing I like about it is that it keeps the consumption code clean. Ideally, you really want to put keep-alive code in the HubEndPoint.OnConnectionAsync.WriteToTransport local function, since that's the central place messages appear. We could have a background task firing pings, but the goal of the ping is to send one if the timeout elapses while waiting for a message. A fixed interval kinda achieves that, but in a weird way and requires synchronization in order to know the time since the last message.

I tried putting this logic into the consumption side (in WriteToTransport) and it gets complicated fast, because KeepAlive isn't always enabled. Though I have another idea I'm going to try.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class as a concept or name??

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably the concept, it's a little sketchy and I'm not super happy with it myself.

@analogrelay
Copy link
Contributor Author

🆙 📅 Tried a different approach to appease @davidfowl (and my own conscience)

@BrennanConroy
Copy link
Member

Spec update in this PR?

@analogrelay
Copy link
Contributor Author

analogrelay commented Nov 28, 2017

No spec update. The only thing needed in the spec is the Ping frame. I'd argue that keep-alive behaviors are policy/config, not spec. I can look at throwing a sentence or two in though.

@BrennanConroy
Copy link
Member

It says "Either endpoint may send a Ping message at any time.", I don't see a good reason the client should be sending pings

@analogrelay
Copy link
Contributor Author

analogrelay commented Nov 28, 2017

There are reasons for an implementation to want that. I'd rather keep the spec open. Just because it says an endpoint "may" do something doesn't mean our implementation has to do it. Also, our server code supports receiving (and ignoring) client pings.

@BrennanConroy
Copy link
Member

:'( easy to shoot yourself in the foot

@analogrelay
Copy link
Contributor Author

How so? There's no mechanism in our client to send a Ping message, and the server is tolerant of receiving them.

@BrennanConroy
Copy link
Member

If someone makes a client exactly like ours except it sends keep-alive, then that will cause the server to stop sending keep-alives and then the client could timeout since it doesn't receive a keep-alive. Not a big problem I guess

@analogrelay
Copy link
Contributor Author

Discussed with @BrennanConroy offline, he was slightly confused and we're all good 😃

@analogrelay
Copy link
Contributor Author

To clarify the behavior to avoid confusion: The keep-alive timer is only reset when a message is sent by the server. Receiving messages from the client does not reset the timer.


Ping messages do not have any payload, they are completely empty messages (aside from the encoding necessary to identify the message as a `Ping` message).

It is up to the server implementation how frequently (if at all) `Ping` frames are sent. The ASP.NET Core implementation sends `Ping` frames only when using the Server Sent Events and WebSockets transports, at a default interval of 15 seconds (configurable). However, a `Ping` frame is only sent if 15 seconds elapses since the last message was sent. Clients may choose to use the "Ping rate" to provide a timeout for the server connection. Since the Client can expect the server to send `Ping` frames at a regular intervals, even when the connection is idle, it can use that to determine if the server has left without closing the connection. The ASP.NET Core implementation (both JavaScript and C#) use a default timeout window of 30 seconds, which is twice the server ping rate interval.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"implementation to decide how frequently"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"at a regular intervals"

{
while (output.Reader.TryRead(out var hubMessage))
var keepAliveTask = needKeepAlive ?
Task.Delay(_hubOptions.KeepAliveInterval) :
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I be concerned that we new up a Task.Delay every send?

Copy link
Contributor Author

@analogrelay analogrelay Nov 28, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not every send. Every time we need to go async to wait for the app to send. And you have to allocate a task for this no matter how you slice it.

@@ -0,0 +1,14 @@
using System;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copyright

@@ -0,0 +1,21 @@
using System;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copy

public async Task ProcessRequestAsync(HttpContext context, CancellationToken token)
public async Task ProcessRequestAsync(ConnectionContext connection, HttpContext context, CancellationToken token)
{
if(connection.Features.Get<IConnectionInherentKeepAliveFeature>() == null)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

space


// Echo a bunch of stuff, waiting 10ms between each, until 500ms have elapsed
DateTime start = DateTime.UtcNow;
while((DateTime.UtcNow - start).TotalMilliseconds <= 500.0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

space

// We shouldn't have any ping messages
HubMessage message;
var counter = 0;
while((message = await client.ReadAsync()) != null)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

space

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OrTimeout

counter += 1;
Assert.IsNotType<PingMessage>(message);
}
Assert.InRange(counter, 1, Int32.MaxValue);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe 50 would be better than max value

using Microsoft.AspNetCore.Sockets.Internal.Transports;
using Microsoft.Extensions.Logging;
using Xunit;
using System;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sort

@analogrelay
Copy link
Contributor Author

🆙 📅

counter += 1;
Assert.Same(PingMessage.Instance, message);
}
Assert.InRange(counter, 1, Int32.MaxValue);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 - 5

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to do 1-10, because I want to make sure this doesn't flake out occasionally due to random delays here and there. Remember that Task.Delay only guarantees that you won't be woken up before the delay has elapsed; you might be sleeping for longer than the delay.

/// </remarks>
public interface IConnectionInherentKeepAliveFeature
{
TimeSpan KeepAliveInterval { get; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this property provide any value? It isn't used currently

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It exposes the duration of the inherent keep-alive (i.e. for Long Polling, the poll timeout value). To be honest, this is just used as a marker interface though, so we could drop it. It seemed like a useful value to have though.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would cleanup the LongPolling constructor a little :P

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But marker interfaces are groooooooss

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm with @BrennanConroy, I don't think this adds any value. Maybe this interface should have a bool so that keep alives can be manually turned off.

@analogrelay
Copy link
Contributor Author

🆙 📅

BrennanConroy
BrennanConroy previously approved these changes Nov 29, 2017
Copy link
Member

@BrennanConroy BrennanConroy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was about to complain about AppVeyor but you saw it too :D

using (var cts = CancellationTokenSource.CreateLinkedTokenSource(timeoutToken, context.RequestAborted))
{
await poll.ProcessRequestAsync(context, cts.Token).OrTimeout();
await poll.ProcessRequestAsync(connection, context, cts.Token).OrTimeout().OrTimeout();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove extra OrTimeout

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the timeout times out?

Copy link
Member

@davidfowl davidfowl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to discuss this in person before you merge

@analogrelay
Copy link
Contributor Author

🆙 📅 @davidfowl - This covers some of our in-person discussions. It needs a little clean-up, but the gist is what we talked about.

// If it is, we send a ping frame, if not, we no-op on this tick. This means that in the worst case, the
// true "ping rate" of the server could be (_hubOptions.KeepAliveInterval + HubEndPoint.KeepAliveTimerInterval),
// because if the interval elapses right after the last tick of this timer, it won't be detected until the next tick.
void KeepAliveTick(object state)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ugh, local functions. We should avoid capturing state.

_lastSendTimestamp = Stopwatch.GetTimestamp();
var writingOutputTask = WriteToTransport();

Timer keepAliveTimer = null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're going to have wayyyyy too many timers. We need a connection heartbeat design here. The only thing that has a connection list is the lifetime manager though. We need to think a bit more here.

Copy link
Member

@davidfowl davidfowl Nov 30, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought we were going to look at a single timer model.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So did I until I realized how HubEndpoint works when reviewing it this morning :). We need to rethink this quite a bit. I think we need to go back to the slightly gross idea of having the connection manager tick the heartbeat via a feature. The problem there is the config gets gross. The ping rate is always going to be off by at most the tick rate and that gets weird.

@analogrelay
Copy link
Contributor Author

🆙 📅 after discussions with @davidfowl, tests don't pass yet, but the pings are indeed sent.

@analogrelay
Copy link
Contributor Author

Pushed some test fixes, but I'm not happy with it. I think the HubLifetimeManager tests are too low-level and brittle. I want to rewrite them to use HubEndpoint but I don't have the time to do that today, I'll look at it tomorrow.

@analogrelay
Copy link
Contributor Author

I gave up trying to make the tests less brittle. It meant refactoring a huge amount of unit tests (because I needed to make TestClient changes to make it clean). I'm sure I could spend more energy and figure out an easier refactor, but I just don't have that kind of time :).

🆙 📅 ready for review again. Tests pass, and we're now using a single heartbeat tick for all connections.

@BrennanConroy BrennanConroy dismissed their stale review December 1, 2017 05:27

Changes happened need to review

@analogrelay analogrelay force-pushed the anurse/1156-server-ping branch from 162c677 to 84f8279 Compare December 4, 2017 18:39
@analogrelay analogrelay dismissed davidfowl’s stale review December 4, 2017 22:43

@davidfowl said to just merge it :)

@analogrelay analogrelay merged commit c1d2024 into dev Dec 4, 2017
@analogrelay analogrelay deleted the anurse/1156-server-ping branch December 13, 2017 00:39
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants