Backport pull request #7732 from jellyfin/release-10.8.z

Fix to make web sockets close gracefully on server shutdown

Authored-by: luke brown <luke92brown@gmail.com>

Merged-by: Cody Robibero <cody@robibe.ro>

Original-merge: ee22feb89a34632a4cc3a350733dd57c6be863ec
This commit is contained in:
Joshua Boniface 2022-07-24 12:35:46 -04:00
parent 8ccd9d8dfa
commit 410871e148
6 changed files with 118 additions and 7 deletions

View File

@ -111,7 +111,7 @@ namespace Emby.Server.Implementations
/// <summary> /// <summary>
/// Class CompositionRoot. /// Class CompositionRoot.
/// </summary> /// </summary>
public abstract class ApplicationHost : IServerApplicationHost, IDisposable public abstract class ApplicationHost : IServerApplicationHost, IAsyncDisposable, IDisposable
{ {
/// <summary> /// <summary>
/// The environment variable prefixes to log at server startup. /// The environment variable prefixes to log at server startup.
@ -1232,5 +1232,49 @@ namespace Emby.Server.Implementations
_disposed = true; _disposed = true;
} }
public async ValueTask DisposeAsync()
{
await DisposeAsyncCore().ConfigureAwait(false);
Dispose(false);
GC.SuppressFinalize(this);
}
/// <summary>
/// Used to perform asynchronous cleanup of managed resources or for cascading calls to <see cref="DisposeAsync"/>.
/// </summary>
/// <returns>A ValueTask.</returns>
protected virtual async ValueTask DisposeAsyncCore()
{
var type = GetType();
Logger.LogInformation("Disposing {Type}", type.Name);
foreach (var (part, _) in _disposableParts)
{
var partType = part.GetType();
if (partType == type)
{
continue;
}
Logger.LogInformation("Disposing {Type}", partType.Name);
try
{
part.Dispose();
}
catch (Exception ex)
{
Logger.LogError(ex, "Error disposing {Type}", partType.Name);
}
}
// used for closing websockets
foreach (var session in _sessionManager.Sessions)
{
await session.DisposeAsync().ConfigureAwait(false);
}
}
} }
} }

View File

@ -19,7 +19,7 @@ namespace Emby.Server.Implementations.HttpServer
/// <summary> /// <summary>
/// Class WebSocketConnection. /// Class WebSocketConnection.
/// </summary> /// </summary>
public class WebSocketConnection : IWebSocketConnection, IDisposable public class WebSocketConnection : IWebSocketConnection
{ {
/// <summary> /// <summary>
/// The logger. /// The logger.
@ -36,6 +36,8 @@ namespace Emby.Server.Implementations.HttpServer
/// </summary> /// </summary>
private readonly WebSocket _socket; private readonly WebSocket _socket;
private bool _disposed = false;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="WebSocketConnection" /> class. /// Initializes a new instance of the <see cref="WebSocketConnection" /> class.
/// </summary> /// </summary>
@ -244,10 +246,39 @@ namespace Emby.Server.Implementations.HttpServer
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
protected virtual void Dispose(bool dispose) protected virtual void Dispose(bool dispose)
{ {
if (_disposed)
{
return;
}
if (dispose) if (dispose)
{ {
_socket.Dispose(); _socket.Dispose();
} }
_disposed = true;
}
/// <inheritdoc />
public async ValueTask DisposeAsync()
{
await DisposeAsyncCore().ConfigureAwait(false);
Dispose(false);
GC.SuppressFinalize(this);
}
/// <summary>
/// Used to perform asynchronous cleanup of managed resources or for cascading calls to <see cref="DisposeAsync"/>.
/// </summary>
/// <returns>A ValueTask.</returns>
protected virtual async ValueTask DisposeAsyncCore()
{
if (_socket.State == WebSocketState.Open)
{
await _socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "System Shutdown", CancellationToken.None).ConfigureAwait(false);
}
_socket.Dispose();
} }
} }
} }

View File

@ -14,7 +14,7 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Session namespace Emby.Server.Implementations.Session
{ {
public sealed class WebSocketController : ISessionController, IDisposable public sealed class WebSocketController : ISessionController, IAsyncDisposable, IDisposable
{ {
private readonly ILogger<WebSocketController> _logger; private readonly ILogger<WebSocketController> _logger;
private readonly ISessionManager _sessionManager; private readonly ISessionManager _sessionManager;
@ -99,6 +99,23 @@ namespace Emby.Server.Implementations.Session
foreach (var socket in _sockets) foreach (var socket in _sockets)
{ {
socket.Closed -= OnConnectionClosed; socket.Closed -= OnConnectionClosed;
socket.Dispose();
}
_disposed = true;
}
public async ValueTask DisposeAsync()
{
if (_disposed)
{
return;
}
foreach (var socket in _sockets)
{
socket.Closed -= OnConnectionClosed;
await socket.DisposeAsync().ConfigureAwait(false);
} }
_disposed = true; _disposed = true;

View File

@ -243,7 +243,7 @@ namespace Jellyfin.Server
} }
} }
appHost.Dispose(); await appHost.DisposeAsync().ConfigureAwait(false);
} }
if (_restartOnShutdown) if (_restartOnShutdown)

View File

@ -10,7 +10,7 @@ using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Controller.Net namespace MediaBrowser.Controller.Net
{ {
public interface IWebSocketConnection public interface IWebSocketConnection : IAsyncDisposable, IDisposable
{ {
/// <summary> /// <summary>
/// Occurs when [closed]. /// Occurs when [closed].

View File

@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Session; using MediaBrowser.Model.Session;
@ -17,7 +18,7 @@ namespace MediaBrowser.Controller.Session
/// <summary> /// <summary>
/// Class SessionInfo. /// Class SessionInfo.
/// </summary> /// </summary>
public sealed class SessionInfo : IDisposable public sealed class SessionInfo : IAsyncDisposable, IDisposable
{ {
// 1 second // 1 second
private const long ProgressIncrement = 10000000; private const long ProgressIncrement = 10000000;
@ -380,10 +381,28 @@ namespace MediaBrowser.Controller.Session
{ {
if (controller is IDisposable disposable) if (controller is IDisposable disposable)
{ {
_logger.LogDebug("Disposing session controller {0}", disposable.GetType().Name); _logger.LogDebug("Disposing session controller synchronously {TypeName}", disposable.GetType().Name);
disposable.Dispose(); disposable.Dispose();
} }
} }
} }
public async ValueTask DisposeAsync()
{
_disposed = true;
StopAutomaticProgress();
var controllers = SessionControllers.ToList();
foreach (var controller in controllers)
{
if (controller is IAsyncDisposable disposableAsync)
{
_logger.LogDebug("Disposing session controller asynchronously {TypeName}", disposableAsync.GetType().Name);
await disposableAsync.DisposeAsync().ConfigureAwait(false);
}
}
}
} }
} }