Skip to content

Commit f2085b1

Browse files
author
Cesar Blum Silveira
committed
Add keep-alive timeout (#464).
1 parent 19f8958 commit f2085b1

File tree

14 files changed

+501
-15
lines changed

14 files changed

+501
-15
lines changed

src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Connection.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5-
using System.Diagnostics;
65
using System.IO;
76
using System.Threading;
87
using System.Threading.Tasks;
@@ -156,6 +155,12 @@ public virtual void OnSocketClosed()
156155
_socketClosedTcs.TrySetResult(null);
157156
}
158157

158+
// Called on Libuv thread
159+
public void Tick()
160+
{
161+
_frame.Tick();
162+
}
163+
159164
private void ApplyConnectionFilter()
160165
{
161166
if (_filterContext.Connection != _libuvStream)
@@ -277,6 +282,11 @@ void IConnectionControl.End(ProduceEndType endType)
277282
}
278283
}
279284

285+
void IConnectionControl.Stop()
286+
{
287+
StopAsync();
288+
}
289+
280290
private static unsafe string GenerateConnectionId(long id)
281291
{
282292
// The following routine is ~310% faster than calling long.ToString() on x64

src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public abstract partial class Frame : ConnectionContext, IFrameControl
5656
private CancellationTokenSource _abortedCts;
5757
private CancellationToken? _manuallySetRequestAbortToken;
5858

59-
protected RequestProcessingStatus _requestProcessingStatus;
59+
private RequestProcessingStatus _requestProcessingStatus;
6060
protected bool _keepAlive;
6161
private bool _autoChunk;
6262
protected Exception _applicationException;
@@ -68,6 +68,8 @@ public abstract partial class Frame : ConnectionContext, IFrameControl
6868
private int _remainingRequestHeadersBytesAllowed;
6969
private int _requestHeadersParsed;
7070

71+
private int _secondsSinceLastRequest;
72+
7173
public Frame(ConnectionContext context)
7274
: base(context)
7375
{
@@ -213,10 +215,7 @@ private CancellationTokenSource RequestAbortedSource
213215
}
214216
}
215217

216-
public bool HasResponseStarted
217-
{
218-
get { return _requestProcessingStatus == RequestProcessingStatus.ResponseStarted; }
219-
}
218+
public bool HasResponseStarted => _requestProcessingStatus == RequestProcessingStatus.ResponseStarted;
220219

221220
protected FrameRequestHeaders FrameRequestHeaders { get; private set; }
222221

@@ -1267,6 +1266,25 @@ protected void ReportApplicationError(Exception ex)
12671266
Log.ApplicationError(ConnectionId, ex);
12681267
}
12691268

1269+
public void Tick()
1270+
{
1271+
// we're in between requests and not about to start processing a new one
1272+
if (_requestProcessingStatus == RequestProcessingStatus.RequestPending && !SocketInput.IsCompleted)
1273+
{
1274+
if (_secondsSinceLastRequest > ServerOptions.Limits.KeepAliveTimeout.TotalSeconds)
1275+
{
1276+
ConnectionControl.Stop();
1277+
}
1278+
1279+
_secondsSinceLastRequest++;
1280+
}
1281+
}
1282+
1283+
public void RequestFinished()
1284+
{
1285+
_secondsSinceLastRequest = 0;
1286+
}
1287+
12701288
protected enum RequestLineStatus
12711289
{
12721290
Empty,
@@ -1277,7 +1295,7 @@ protected enum RequestLineStatus
12771295
Done
12781296
}
12791297

1280-
protected enum RequestProcessingStatus
1298+
private enum RequestProcessingStatus
12811299
{
12821300
RequestPending,
12831301
RequestStarted,

src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/IConnectionControl.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@ public interface IConnectionControl
88
void Pause();
99
void Resume();
1010
void End(ProduceEndType endType);
11+
void Stop();
1112
}
1213
}

src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/MessageBody.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ public override ValueTask<int> ReadAsyncImplementation(ArraySegment<byte> buffer
172172
var limit = buffer.Array == null ? inputLengthLimit : Math.Min(buffer.Count, inputLengthLimit);
173173
if (limit == 0)
174174
{
175+
_context.RequestFinished();
175176
return new ValueTask<int>(0);
176177
}
177178

@@ -182,10 +183,17 @@ public override ValueTask<int> ReadAsyncImplementation(ArraySegment<byte> buffer
182183
// .GetAwaiter().GetResult() done by ValueTask if needed
183184
var actual = task.Result;
184185
_inputLength -= actual;
186+
185187
if (actual == 0)
186188
{
187189
_context.RejectRequest(RequestRejectionReason.UnexpectedEndOfRequestContent);
188190
}
191+
192+
if (_inputLength == 0)
193+
{
194+
_context.RequestFinished();
195+
}
196+
189197
return new ValueTask<int>(actual);
190198
}
191199
else
@@ -198,11 +206,17 @@ private async Task<int> ReadAsyncAwaited(Task<int> task)
198206
{
199207
var actual = await task;
200208
_inputLength -= actual;
209+
201210
if (actual == 0)
202211
{
203212
_context.RejectRequest(RequestRejectionReason.UnexpectedEndOfRequestContent);
204213
}
205214

215+
if (_inputLength == 0)
216+
{
217+
_context.RequestFinished();
218+
}
219+
206220
return actual;
207221
}
208222
}
@@ -354,6 +368,8 @@ private async Task<int> ReadStateMachineAsync(SocketInput input, ArraySegment<by
354368
_mode = Mode.Complete;
355369
}
356370

371+
_context.RequestFinished();
372+
357373
return 0;
358374
}
359375

src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/KestrelThread.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,16 @@ public class KestrelThread
2727
// otherwise it needs to wait till the next pass of the libuv loop
2828
private readonly int _maxLoops = 8;
2929

30+
// how often the heartbeat timer will tick connections
31+
private const int _heartbeatMilliseconds = 1000;
32+
3033
private readonly KestrelEngine _engine;
3134
private readonly IApplicationLifetime _appLifetime;
3235
private readonly Thread _thread;
3336
private readonly TaskCompletionSource<object> _threadTcs = new TaskCompletionSource<object>();
3437
private readonly UvLoopHandle _loop;
3538
private readonly UvAsyncHandle _post;
39+
private readonly UvTimerHandle _heartbeatTimer;
3640
private Queue<Work> _workAdding = new Queue<Work>(1024);
3741
private Queue<Work> _workRunning = new Queue<Work>(1024);
3842
private Queue<CloseHandle> _closeHandleAdding = new Queue<CloseHandle>(256);
@@ -57,6 +61,7 @@ public KestrelThread(KestrelEngine engine)
5761
_post = new UvAsyncHandle(_log);
5862
_thread = new Thread(ThreadStart);
5963
_thread.Name = "KestrelThread - libuv";
64+
_heartbeatTimer = new UvTimerHandle(_log);
6065
#if !DEBUG
6166
// Mark the thread as being as unimportant to keeping the process alive.
6267
// Don't do this for debug builds, so we know if the thread isn't terminating.
@@ -176,9 +181,9 @@ private async Task DisposeConnectionsAsync()
176181
}
177182
}
178183

179-
180184
private void AllowStop()
181185
{
186+
_heartbeatTimer.Stop();
182187
_post.Unreference();
183188
}
184189

@@ -274,6 +279,8 @@ private void ThreadStart(object parameter)
274279
{
275280
_loop.Init(_engine.Libuv);
276281
_post.Init(_loop, OnPost, EnqueueCloseHandle);
282+
_heartbeatTimer.Init(_loop, EnqueueCloseHandle);
283+
_heartbeatTimer.Start(OnHeartbeat, timeout: 1000, repeat: 1000);
277284
_initCompleted = true;
278285
tcs.SetResult(0);
279286
}
@@ -296,6 +303,7 @@ private void ThreadStart(object parameter)
296303
// run the loop one more time to delete the open handles
297304
_post.Reference();
298305
_post.Dispose();
306+
_heartbeatTimer.Dispose();
299307

300308
// Ensure the Dispose operations complete in the event loop.
301309
_loop.Run();
@@ -327,6 +335,15 @@ private void OnPost()
327335
} while (wasWork && loopsRemaining > 0);
328336
}
329337

338+
private void OnHeartbeat(UvTimerHandle timer)
339+
{
340+
Walk(ptr =>
341+
{
342+
var handle = UvMemory.FromIntPtr<UvHandle>(ptr);
343+
(handle as UvStreamHandle)?.Connection?.Tick();
344+
});
345+
}
346+
330347
private bool DoPostWork()
331348
{
332349
Queue<Work> queue;

src/Microsoft.AspNetCore.Server.Kestrel/Internal/Networking/Libuv.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ public Libuv()
5353
_uv_tcp_getpeername = NativeMethods.uv_tcp_getpeername;
5454
_uv_tcp_getsockname = NativeMethods.uv_tcp_getsockname;
5555
_uv_walk = NativeMethods.uv_walk;
56+
_uv_timer_init = NativeMethods.uv_timer_init;
57+
_uv_timer_start = NativeMethods.uv_timer_start;
58+
_uv_timer_stop = NativeMethods.uv_timer_stop;
5659
}
5760

5861
// Second ctor that doesn't set any fields only to be used by MockLibuv
@@ -407,6 +410,30 @@ public void walk(UvLoopHandle loop, uv_walk_cb walk_cb, IntPtr arg)
407410
_uv_walk(loop, walk_cb, arg);
408411
}
409412

413+
protected Func<UvLoopHandle, UvTimerHandle, int> _uv_timer_init;
414+
unsafe public void timer_init(UvLoopHandle loop, UvTimerHandle handle)
415+
{
416+
loop.Validate();
417+
handle.Validate();
418+
ThrowIfErrored(_uv_timer_init(loop, handle));
419+
}
420+
421+
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
422+
public delegate void uv_timer_cb(IntPtr handle);
423+
protected Func<UvTimerHandle, uv_timer_cb, long, long, int> _uv_timer_start;
424+
unsafe public void timer_start(UvTimerHandle handle, uv_timer_cb cb, long timeout, long repeat)
425+
{
426+
handle.Validate();
427+
ThrowIfErrored(_uv_timer_start(handle, cb, timeout, repeat));
428+
}
429+
430+
protected Func<UvTimerHandle, int> _uv_timer_stop;
431+
unsafe public void timer_stop(UvTimerHandle handle)
432+
{
433+
handle.Validate();
434+
ThrowIfErrored(_uv_timer_stop(handle));
435+
}
436+
410437
public delegate int uv_tcp_getsockname_func(UvTcpHandle handle, out SockAddr addr, ref int namelen);
411438
protected uv_tcp_getsockname_func _uv_tcp_getsockname;
412439
public void tcp_getsockname(UvTcpHandle handle, out SockAddr addr, ref int namelen)
@@ -604,6 +631,15 @@ private static class NativeMethods
604631
[DllImport("libuv", CallingConvention = CallingConvention.Cdecl)]
605632
public static extern int uv_walk(UvLoopHandle loop, uv_walk_cb walk_cb, IntPtr arg);
606633

634+
[DllImport("libuv", CallingConvention = CallingConvention.Cdecl)]
635+
unsafe public static extern int uv_timer_init(UvLoopHandle loop, UvTimerHandle handle);
636+
637+
[DllImport("libuv", CallingConvention = CallingConvention.Cdecl)]
638+
unsafe public static extern int uv_timer_start(UvTimerHandle handle, uv_timer_cb cb, long timeout, long repeat);
639+
640+
[DllImport("libuv", CallingConvention = CallingConvention.Cdecl)]
641+
unsafe public static extern int uv_timer_stop(UvTimerHandle handle);
642+
607643
[DllImport("WS2_32.dll", CallingConvention = CallingConvention.Winapi)]
608644
unsafe public static extern int WSAIoctl(
609645
IntPtr socket,
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
6+
using Microsoft.Extensions.Logging;
7+
8+
namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Networking
9+
{
10+
public class UvTimerHandle : UvHandle
11+
{
12+
private readonly static Libuv.uv_timer_cb _uv_timer_cb = UvTimerCb;
13+
14+
private Action<UvTimerHandle> _callback;
15+
16+
public UvTimerHandle(IKestrelTrace logger) : base(logger)
17+
{
18+
}
19+
20+
public void Init(UvLoopHandle loop, Action<Action<IntPtr>, IntPtr> queueCloseHandle)
21+
{
22+
CreateHandle(
23+
loop.Libuv,
24+
loop.ThreadId,
25+
loop.Libuv.handle_size(Libuv.HandleType.TIMER),
26+
queueCloseHandle);
27+
28+
_uv.timer_init(loop, this);
29+
}
30+
31+
public void Start(Action<UvTimerHandle> callback, long timeout, long repeat)
32+
{
33+
_callback = callback;
34+
_uv.timer_start(this, _uv_timer_cb, timeout, repeat);
35+
}
36+
37+
public void Stop()
38+
{
39+
_uv.timer_stop(this);
40+
}
41+
42+
private static void UvTimerCb(IntPtr handle)
43+
{
44+
var timer = FromIntPtr<UvTimerHandle>(handle);
45+
46+
try
47+
{
48+
timer._callback(timer);
49+
}
50+
catch (Exception ex)
51+
{
52+
timer._log.LogError(0, ex, nameof(UvTimerCb));
53+
throw;
54+
}
55+
}
56+
}
57+
}

src/Microsoft.AspNetCore.Server.Kestrel/KestrelServerLimits.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ public class KestrelServerLimits
2323
// Matches the default LimitRequestFields in Apache httpd.
2424
private int _maxRequestHeaderCount = 100;
2525

26+
// Matches the default http.sys keep-alive timouet.
27+
private TimeSpan _keepAliveTimeout = TimeSpan.FromMinutes(2);
28+
2629
/// <summary>
2730
/// Gets or sets the maximum size of the response buffer before write
2831
/// calls begin to block or return tasks that don't complete until the
@@ -138,5 +141,23 @@ public int MaxRequestHeaderCount
138141
_maxRequestHeaderCount = value;
139142
}
140143
}
144+
145+
/// <summary>
146+
/// Gets or sets the keep-alive timeout.
147+
/// </summary>
148+
/// <remarks>
149+
/// Defaults to 2 minutes. Timeout granularity is in seconds. Sub-second values will be rounded to the next second.
150+
/// </remarks>
151+
public TimeSpan KeepAliveTimeout
152+
{
153+
get
154+
{
155+
return _keepAliveTimeout;
156+
}
157+
set
158+
{
159+
_keepAliveTimeout = TimeSpan.FromSeconds(Math.Ceiling(value.TotalSeconds));
160+
}
161+
}
141162
}
142163
}

0 commit comments

Comments
 (0)