瀏覽代碼

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
Joshua Boniface 2 年之前
父節點
當前提交
410871e148

+ 45 - 1
Emby.Server.Implementations/ApplicationHost.cs

@@ -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);
+            }
+        }
     }
     }
 }
 }

+ 32 - 1
Emby.Server.Implementations/HttpServer/WebSocketConnection.cs

@@ -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();
         }
         }
     }
     }
 }
 }

+ 18 - 1
Emby.Server.Implementations/Session/WebSocketController.cs

@@ -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;

+ 1 - 1
Jellyfin.Server/Program.cs

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

+ 1 - 1
MediaBrowser.Controller/Net/IWebSocketConnection.cs

@@ -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].

+ 21 - 2
MediaBrowser.Controller/Session/SessionInfo.cs

@@ -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);
+                }
+            }
+        }
     }
     }
 }
 }