From 730846754c13d6bb53995c7885383765ceb2ba9a Mon Sep 17 00:00:00 2001 From: JialinXin Date: Wed, 19 Feb 2020 10:59:44 +0800 Subject: [PATCH 01/14] update dev version to 1.4.1 (#828) --- version.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.props b/version.props index 7e1e87949..a738a9c34 100644 --- a/version.props +++ b/version.props @@ -1,6 +1,6 @@ - 1.4.0 + 1.4.1 preview1 $(VersionPrefix) $(VersionPrefix)-$(VersionSuffix)-final From 8aba3d23ec3653528ce122deb729405036111b11 Mon Sep 17 00:00:00 2001 From: JialinXin Date: Wed, 19 Feb 2020 17:39:42 +0800 Subject: [PATCH 02/14] [Part3.1][Live-scale] Minor refactor prepare for live-scale (#818) * refactor prepare for live-scale * typo fix * revert incorrect constants replace * resolve comments * remove unused constants in the PR. * uniform behavior when write failure. * move check. * resolve comments * improve naming --- .../Constants.cs | 2 +- ...MultiEndpointServiceConnectionContainer.cs | 9 ++ .../IServiceConnectionContainerFactory.cs | 8 +- ...MultiEndpointServiceConnectionContainer.cs | 109 ++++++---------- .../ServiceConnectionContainerFactory.cs | 2 +- .../WeakServiceConnectionContainer.cs | 2 +- .../HubHost/ServiceHubDispatcher.cs | 2 +- ...EndpointServiceConnectionContainerTests.cs | 75 +++++++++-- .../TestServiceConnectionContainer.cs | 5 +- .../TestServiceConnectionContainerFactory.cs | 2 +- ...EndpointServiceConnectionContainerTests.cs | 117 +++++++++++------- 11 files changed, 198 insertions(+), 135 deletions(-) create mode 100644 src/Microsoft.Azure.SignalR.Common/Interfaces/IMultiEndpointServiceConnectionContainer.cs diff --git a/src/Microsoft.Azure.SignalR.Common/Constants.cs b/src/Microsoft.Azure.SignalR.Common/Constants.cs index 83349c51b..70c7b0277 100644 --- a/src/Microsoft.Azure.SignalR.Common/Constants.cs +++ b/src/Microsoft.Azure.SignalR.Common/Constants.cs @@ -12,7 +12,7 @@ internal static class Constants public const string ApplicationNameDefaultKey = "Azure:SignalR:ApplicationName"; public const int DefaultShutdownTimeoutInSeconds = 30; - public const int DefaultScaleTimeoutInSeconds = 300; + public const int DefaultStatusPingIntervalInSeconds = 10; public const string AsrsMigrateFrom = "Asrs-Migrate-From"; public const string AsrsMigrateTo = "Asrs-Migrate-To"; diff --git a/src/Microsoft.Azure.SignalR.Common/Interfaces/IMultiEndpointServiceConnectionContainer.cs b/src/Microsoft.Azure.SignalR.Common/Interfaces/IMultiEndpointServiceConnectionContainer.cs new file mode 100644 index 000000000..c50c22f11 --- /dev/null +++ b/src/Microsoft.Azure.SignalR.Common/Interfaces/IMultiEndpointServiceConnectionContainer.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Azure.SignalR +{ + internal interface IMultiEndpointServiceConnectionContainer : IServiceConnectionContainer + { + } +} diff --git a/src/Microsoft.Azure.SignalR.Common/Interfaces/IServiceConnectionContainerFactory.cs b/src/Microsoft.Azure.SignalR.Common/Interfaces/IServiceConnectionContainerFactory.cs index 286fdbcb5..e7c481fcf 100644 --- a/src/Microsoft.Azure.SignalR.Common/Interfaces/IServiceConnectionContainerFactory.cs +++ b/src/Microsoft.Azure.SignalR.Common/Interfaces/IServiceConnectionContainerFactory.cs @@ -1,12 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. namespace Microsoft.Azure.SignalR { interface IServiceConnectionContainerFactory { - IServiceConnectionContainer Create(string hub); + IMultiEndpointServiceConnectionContainer Create(string hub); } } diff --git a/src/Microsoft.Azure.SignalR.Common/ServiceConnections/MultiEndpointServiceConnectionContainer.cs b/src/Microsoft.Azure.SignalR.Common/ServiceConnections/MultiEndpointServiceConnectionContainer.cs index e05b00a52..13e893806 100644 --- a/src/Microsoft.Azure.SignalR.Common/ServiceConnections/MultiEndpointServiceConnectionContainer.cs +++ b/src/Microsoft.Azure.SignalR.Common/ServiceConnections/MultiEndpointServiceConnectionContainer.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -13,15 +14,19 @@ namespace Microsoft.Azure.SignalR { - internal class MultiEndpointServiceConnectionContainer : IServiceConnectionContainer - { + internal class MultiEndpointServiceConnectionContainer : IMultiEndpointServiceConnectionContainer + { + private readonly ConcurrentDictionary _connectionContainers = + new ConcurrentDictionary(); + private readonly IMessageRouter _router; private readonly ILogger _logger; - private readonly IServiceConnectionContainer _inner; - private IReadOnlyList _endpoints; + // + private (bool needRouter, IReadOnlyList endpoints) _routerEndpoints; - public Dictionary ConnectionContainers { get; } + // for test use + public IReadOnlyDictionary ConnectionContainers => _connectionContainers; internal MultiEndpointServiceConnectionContainer( string hub, @@ -33,22 +38,21 @@ internal MultiEndpointServiceConnectionContainer( if (generator == null) { throw new ArgumentNullException(nameof(generator)); - } - + } + + _router = router ?? throw new ArgumentNullException(nameof(router)); _logger = loggerFactory?.CreateLogger() ?? throw new ArgumentNullException(nameof(loggerFactory)); // provides a copy to the endpoint per container - _endpoints = endpointManager.GetEndpoints(hub); + var endpoints = endpointManager.GetEndpoints(hub); + // router will be used when there's customized MessageRouter or multiple endpoints + var needRouter = endpoints.Count > 1 || !(_router is DefaultMessageRouter); + + _routerEndpoints = (needRouter, endpoints); - if (_endpoints.Count == 1) - { - _inner = generator(_endpoints[0]); - } - else - { - // router is required when endpoints > 1 - _router = router ?? throw new ArgumentNullException(nameof(router)); - ConnectionContainers = _endpoints.ToDictionary(s => (ServiceEndpoint)s, s => generator(s)); + foreach (var endpoint in endpoints) + { + _connectionContainers[endpoint] = generator(endpoint); } } @@ -71,7 +75,7 @@ ILoggerFactory loggerFactory public IEnumerable GetOnlineEndpoints() { - return ConnectionContainers.Keys.Where(s => s.Online); + return _connectionContainers.Where(s => s.Key.Online).Select(s => s.Key); } private static IServiceConnectionContainer CreateContainer(IServiceConnectionFactory serviceConnectionFactory, HubServiceEndpoint endpoint, int count, ILoggerFactory loggerFactory) @@ -92,24 +96,14 @@ public Task ConnectionInitializedTask { get { - if (_inner != null) - { - return _inner.ConnectionInitializedTask; - } - - return Task.WhenAll(from connection in ConnectionContainers + return Task.WhenAll(from connection in _connectionContainers select connection.Value.ConnectionInitializedTask); } } public Task StartAsync() { - if (_inner != null) - { - return _inner.StartAsync(); - } - - return Task.WhenAll(ConnectionContainers.Select(s => + return Task.WhenAll(_connectionContainers.Select(s => { Log.StartingConnection(_logger, s.Key.Endpoint); return s.Value.StartAsync(); @@ -118,12 +112,7 @@ public Task StartAsync() public Task StopAsync() { - if (_inner != null) - { - return _inner.StopAsync(); - } - - return Task.WhenAll(ConnectionContainers.Select(s => + return Task.WhenAll(_connectionContainers.Select(s => { Log.StoppingConnection(_logger, s.Key.Endpoint); return s.Value.StopAsync(); @@ -132,32 +121,16 @@ public Task StopAsync() public Task OfflineAsync(bool migratable) { - if (_inner != null) - { - return _inner.OfflineAsync(migratable); - } - else - { - return Task.WhenAll(ConnectionContainers.Select(c => c.Value.OfflineAsync(migratable))); - } + return Task.WhenAll(_connectionContainers.Select(c => c.Value.OfflineAsync(migratable))); } public Task WriteAsync(ServiceMessage serviceMessage) { - if (_inner != null) - { - return _inner.WriteAsync(serviceMessage); - } return WriteMultiEndpointMessageAsync(serviceMessage, connection => connection.WriteAsync(serviceMessage)); } public async Task WriteAckableMessageAsync(ServiceMessage serviceMessage, CancellationToken cancellationToken = default) { - if (_inner != null) - { - return await _inner.WriteAckableMessageAsync(serviceMessage, cancellationToken); - } - // If we have multiple endpoints, we should wait to one of the following conditions hit // 1. One endpoint responses "OK" state // 2. All the endpoints response failed state including "NotFound", "Timeout" and waiting response to timeout @@ -183,7 +156,11 @@ public async Task WriteAckableMessageAsync(ServiceMessage serviceMessage, internal IEnumerable GetRoutedEndpoints(ServiceMessage message) { - var endpoints = _endpoints; + if (!_routerEndpoints.needRouter) + { + return _routerEndpoints.endpoints; + } + var endpoints = _routerEndpoints.endpoints; switch (message) { case BroadcastDataMessage bdm: @@ -214,7 +191,7 @@ private Task WriteMultiEndpointMessageAsync(ServiceMessage serviceMessage, Func< var routed = GetRoutedEndpoints(serviceMessage)? .Select(endpoint => { - if (ConnectionContainers.TryGetValue(endpoint, out var connection)) + if (_connectionContainers.TryGetValue(endpoint, out var connection)) { return (e: endpoint, c: connection); } @@ -225,14 +202,14 @@ private Task WriteMultiEndpointMessageAsync(ServiceMessage serviceMessage, Func< .Where(c => c.c != null) .Select(async s => { - try + try { - await inner(s.c); + await inner(s.c); } catch (ServiceConnectionNotActiveException) - { - // log and don't stop other endpoints - Log.FailedWritingMessageToEndpoint(_logger, serviceMessage.GetType().Name, s.e.ToString()); + { + // log and don't stop other endpoints + Log.FailedWritingMessageToEndpoint(_logger, serviceMessage.GetType().Name, s.e.ToString()); } }).ToArray(); @@ -263,14 +240,11 @@ private static class Log LoggerMessage.Define(LogLevel.Error, new EventId(3, "EndpointNotExists"), "Endpoint {endpoint} from the router does not exists."); private static readonly Action _noEndpointRouted = - LoggerMessage.Define(LogLevel.Warning, new EventId(4, "NoEndpointRouted"), "Message {messageType} is not sent because no endpoint is returned from the endpoint router."); - + LoggerMessage.Define(LogLevel.Warning, new EventId(4, "NoEndpointRouted"), "Message {messageType} is not sent because no endpoint is returned from the endpoint router."); + private static readonly Action _failedWritingMessageToEndpoint = LoggerMessage.Define(LogLevel.Warning, new EventId(5, "FailedWritingMessageToEndpoint"), "Message {messageType} is not sent to endpoint {endpoint} because all connections to this endpoint are offline."); - private static readonly Action _closingConnection = - LoggerMessage.Define(LogLevel.Debug, new EventId(6, "ClosingConnection"), "Closing connections for endpoint {endpoint}."); - public static void StartingConnection(ILogger logger, string endpoint) { _startingConnection(logger, endpoint, null); @@ -281,11 +255,6 @@ public static void StoppingConnection(ILogger logger, string endpoint) _stoppingConnection(logger, endpoint, null); } - public static void ClosingConnection(ILogger logger, string endpoint) - { - _closingConnection(logger, endpoint, null); - } - public static void EndpointNotExists(ILogger logger, string endpoint) { _endpointNotExists(logger, endpoint, null); diff --git a/src/Microsoft.Azure.SignalR.Common/ServiceConnections/ServiceConnectionContainerFactory.cs b/src/Microsoft.Azure.SignalR.Common/ServiceConnections/ServiceConnectionContainerFactory.cs index f922d43d7..25043fc6f 100644 --- a/src/Microsoft.Azure.SignalR.Common/ServiceConnections/ServiceConnectionContainerFactory.cs +++ b/src/Microsoft.Azure.SignalR.Common/ServiceConnections/ServiceConnectionContainerFactory.cs @@ -28,7 +28,7 @@ public ServiceConnectionContainerFactory( _loggerFactory = loggerFactory; } - public IServiceConnectionContainer Create(string hub) + public IMultiEndpointServiceConnectionContainer Create(string hub) { return new MultiEndpointServiceConnectionContainer(_serviceConnectionFactory, hub, _options.ConnectionCount, _serviceEndpointManager, _router, _loggerFactory); } diff --git a/src/Microsoft.Azure.SignalR.Common/ServiceConnections/WeakServiceConnectionContainer.cs b/src/Microsoft.Azure.SignalR.Common/ServiceConnections/WeakServiceConnectionContainer.cs index 5234d2a6b..7b6b8413a 100644 --- a/src/Microsoft.Azure.SignalR.Common/ServiceConnections/WeakServiceConnectionContainer.cs +++ b/src/Microsoft.Azure.SignalR.Common/ServiceConnections/WeakServiceConnectionContainer.cs @@ -13,7 +13,7 @@ namespace Microsoft.Azure.SignalR.Common.ServiceConnections internal class WeakServiceConnectionContainer : ServiceConnectionContainerBase { private const int CheckWindow = 5; - private static readonly TimeSpan DefaultGetServiceStatusInterval = TimeSpan.FromSeconds(10); + private static readonly TimeSpan DefaultGetServiceStatusInterval = TimeSpan.FromSeconds(Constants.DefaultStatusPingIntervalInSeconds); private static readonly long DefaultGetServiceStatusTicks = DefaultGetServiceStatusInterval.Seconds * Stopwatch.Frequency; private static readonly TimeSpan CheckTimeSpan = TimeSpan.FromMinutes(10); diff --git a/src/Microsoft.Azure.SignalR/HubHost/ServiceHubDispatcher.cs b/src/Microsoft.Azure.SignalR/HubHost/ServiceHubDispatcher.cs index b6b827753..3b405cda2 100644 --- a/src/Microsoft.Azure.SignalR/HubHost/ServiceHubDispatcher.cs +++ b/src/Microsoft.Azure.SignalR/HubHost/ServiceHubDispatcher.cs @@ -90,7 +90,7 @@ private async Task OfflineAndWaitForCompletedAsync(bool migratable) await _clientConnectionManager.WhenAllCompleted(); } - private IServiceConnectionContainer GetMultiEndpointServiceConnectionContainer(string hub, ConnectionDelegate connectionDelegate, Action contextConfig = null) + private IMultiEndpointServiceConnectionContainer GetMultiEndpointServiceConnectionContainer(string hub, ConnectionDelegate connectionDelegate, Action contextConfig = null) { var connectionFactory = new ConnectionFactory(_nameProvider, _loggerFactory); var serviceConnectionFactory = new ServiceConnectionFactory(_serviceProtocol, diff --git a/test/Microsoft.Azure.SignalR.AspNet.Tests/MultiEndpointServiceConnectionContainerTests.cs b/test/Microsoft.Azure.SignalR.AspNet.Tests/MultiEndpointServiceConnectionContainerTests.cs index a7393bef4..6ef024e04 100644 --- a/test/Microsoft.Azure.SignalR.AspNet.Tests/MultiEndpointServiceConnectionContainerTests.cs +++ b/test/Microsoft.Azure.SignalR.AspNet.Tests/MultiEndpointServiceConnectionContainerTests.cs @@ -198,7 +198,7 @@ public async Task TestEndpointManagerWithDuplicateEndpointsAndConnectionStarted( _ = container.StartAsync(); await container.ConnectionInitializedTask.OrTimeout(); - endpoints = container.GetOnlineEndpoints().ToArray(); + endpoints = container.GetOnlineEndpoints().OrderBy(x => x.Name).ToArray(); Assert.Equal(2, endpoints.Length); Assert.Equal("1", endpoints[0].Name); Assert.Equal("11", endpoints[1].Name); @@ -226,7 +226,7 @@ public void TestContainerWithNoPrimaryEndpointDefinedThrows() public async Task TestContainerWithOneEndpointWithAllConnectedSucceeeds() { var sem = new TestServiceEndpointManager(new ServiceEndpoint(ConnectionString1)); - var router = new TestEndpointRouter(true); + var router = new DefaultEndpointRouter(); var container = new TestMultiEndpointServiceConnectionContainer("hub", e => new TestBaseServiceConnectionContainer(new List { new TestServiceConnection(), @@ -244,31 +244,84 @@ public async Task TestContainerWithOneEndpointWithAllConnectedSucceeeds() await container.WriteAsync(DefaultGroupMessage); } + [Fact] + public async Task TestContainerWithOneEndpointCustomizeRouterWithAllConnectedSucceeeds() + { + var sem = new TestServiceEndpointManager(new ServiceEndpoint(ConnectionString1)); + var router = new TestEndpointRouter(false); + var container = new TestMultiEndpointServiceConnectionContainer("hub", + e => new TestBaseServiceConnectionContainer(new List { + new TestServiceConnection(), + new TestServiceConnection(), + new TestServiceConnection(), + new TestServiceConnection(), + new TestServiceConnection(), + new TestServiceConnection(), + new TestServiceConnection(), + }, e), sem, router, NullLoggerFactory.Instance); + + _ = container.StartAsync(); + await container.ConnectionInitializedTask.OrTimeout(); + + await container.WriteAsync(DefaultGroupMessage); + } [Fact] - public async Task TestContainerWithOneEndpointWithAllDisconnectedAndConnectionStartedThrows() + public async Task TestContainerWithOneEndpointBadRouterWithConnectionStartedThrows() { var sem = new TestServiceEndpointManager(new ServiceEndpoint(ConnectionString1)); var router = new TestEndpointRouter(true); var container = new TestMultiEndpointServiceConnectionContainer("hub", e => new TestBaseServiceConnectionContainer(new List { - new TestServiceConnection(ServiceConnectionStatus.Disconnected), - new TestServiceConnection(ServiceConnectionStatus.Disconnected), - new TestServiceConnection(ServiceConnectionStatus.Disconnected), - new TestServiceConnection(ServiceConnectionStatus.Disconnected), - new TestServiceConnection(ServiceConnectionStatus.Disconnected), - new TestServiceConnection(ServiceConnectionStatus.Disconnected), - new TestServiceConnection(ServiceConnectionStatus.Disconnected), + new TestServiceConnection(), + new TestServiceConnection(), + new TestServiceConnection(), + new TestServiceConnection(), + new TestServiceConnection(), + new TestServiceConnection(), + new TestServiceConnection(), }, e), sem, router, NullLoggerFactory.Instance); _ = container.StartAsync(); await container.ConnectionInitializedTask.OrTimeout(); - await Assert.ThrowsAsync( + + await Assert.ThrowsAsync( () => container.WriteAsync(DefaultGroupMessage) ); } + [Fact] + public async Task TestContainerWithOneEndpointWithAllDisconnectedAndWriteMessageSucceedWithWarning() + { + using (StartVerifiableLog(out var loggerFactory, LogLevel.Warning, logChecker: logs => + { + var warns = logs.Where(s => s.Write.LogLevel == LogLevel.Warning).ToList(); + Assert.Single(warns); + Assert.Contains(warns, s => s.Write.Message.Contains("Message JoinGroupWithAckMessage is not sent to endpoint (Primary)http://url1 because all connections to this endpoint are offline.")); + return true; + })) + { + var sem = new TestServiceEndpointManager(new ServiceEndpoint(ConnectionString1)); + var router = new DefaultEndpointRouter(); + var container = new TestMultiEndpointServiceConnectionContainer("hub", + e => new TestBaseServiceConnectionContainer(new List { + new TestServiceConnection(ServiceConnectionStatus.Disconnected), + new TestServiceConnection(ServiceConnectionStatus.Disconnected), + new TestServiceConnection(ServiceConnectionStatus.Disconnected), + new TestServiceConnection(ServiceConnectionStatus.Disconnected), + new TestServiceConnection(ServiceConnectionStatus.Disconnected), + new TestServiceConnection(ServiceConnectionStatus.Disconnected), + new TestServiceConnection(ServiceConnectionStatus.Disconnected), + }, e), sem, router, loggerFactory); + + _ = container.StartAsync(); + await container.ConnectionInitializedTask.OrTimeout(); + + await container.WriteAsync(DefaultGroupMessage); + } + } + [Fact] public async Task TestContainerWithTwoEndpointWithAllConnectedFailsWithBadRouter() { diff --git a/test/Microsoft.Azure.SignalR.Tests.Common/TestClasses/TestServiceConnectionContainer.cs b/test/Microsoft.Azure.SignalR.Tests.Common/TestClasses/TestServiceConnectionContainer.cs index 2ce91f445..469ebf0fe 100644 --- a/test/Microsoft.Azure.SignalR.Tests.Common/TestClasses/TestServiceConnectionContainer.cs +++ b/test/Microsoft.Azure.SignalR.Tests.Common/TestClasses/TestServiceConnectionContainer.cs @@ -2,13 +2,14 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.Azure.SignalR.Protocol; namespace Microsoft.Azure.SignalR.Tests.Common { - internal sealed class TestServiceConnectionContainer : IServiceConnectionContainer, IServiceConnection + internal sealed class TestServiceConnectionContainer : IServiceConnectionContainer, IServiceConnection, IMultiEndpointServiceConnectionContainer { private readonly Action<(ServiceMessage, IServiceConnectionContainer)> _validator; @@ -22,6 +23,8 @@ internal sealed class TestServiceConnectionContainer : IServiceConnectionContain public Task ConnectionOfflineTask => Task.CompletedTask; + public IReadOnlyDictionary ConnectionContainers { get; } + public TestServiceConnectionContainer(ServiceConnectionStatus status) { Status = status; diff --git a/test/Microsoft.Azure.SignalR.Tests.Common/TestClasses/TestServiceConnectionContainerFactory.cs b/test/Microsoft.Azure.SignalR.Tests.Common/TestClasses/TestServiceConnectionContainerFactory.cs index ff2e780e5..98bc345dd 100644 --- a/test/Microsoft.Azure.SignalR.Tests.Common/TestClasses/TestServiceConnectionContainerFactory.cs +++ b/test/Microsoft.Azure.SignalR.Tests.Common/TestClasses/TestServiceConnectionContainerFactory.cs @@ -13,7 +13,7 @@ public TestServiceConnectionContainerFactory(SortedList { _messages = output; } - public IServiceConnectionContainer Create(string hub) + public IMultiEndpointServiceConnectionContainer Create(string hub) { return new TestServiceConnectionContainer(hub, m => diff --git a/test/Microsoft.Azure.SignalR.Tests/MultiEndpointServiceConnectionContainerTests.cs b/test/Microsoft.Azure.SignalR.Tests/MultiEndpointServiceConnectionContainerTests.cs index c9c2e4aa5..b31878368 100644 --- a/test/Microsoft.Azure.SignalR.Tests/MultiEndpointServiceConnectionContainerTests.cs +++ b/test/Microsoft.Azure.SignalR.Tests/MultiEndpointServiceConnectionContainerTests.cs @@ -19,20 +19,7 @@ namespace Microsoft.Azure.SignalR.Tests { public class TestEndpointServiceConnectionContainerTests : VerifiableLoggedTest - { - private sealed class TestMultiEndpointServiceConnectionContainer : MultiEndpointServiceConnectionContainer - { - public TestMultiEndpointServiceConnectionContainer(string hub, - Func generator, - IServiceEndpointManager endpoint, - IEndpointRouter router, - ILoggerFactory loggerFactory - - ) : base(hub, generator, endpoint, router, loggerFactory) - { - } - } - + { private const string ConnectionStringFormatter = "Endpoint={0};AccessKey=ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789;"; private const string Url1 = "http://url1"; private const string Url2 = "https://url2"; @@ -122,7 +109,7 @@ public async Task TestEndpointManagerWithDuplicateEndpointsAndConnectionStarted( _ = container.StartAsync(); await container.ConnectionInitializedTask; - endpoints = container.GetOnlineEndpoints().ToArray(); + endpoints = container.GetOnlineEndpoints().OrderBy(x => x.Name).ToArray(); Assert.Equal(2, endpoints.Length); Assert.Equal("1", endpoints[0].Name); Assert.Equal("11", endpoints[1].Name); @@ -150,8 +137,7 @@ public void TestContainerWithNoPrimaryEndpointDefinedThrows() public async Task TestContainerWithOneEndpointWithAllConnectedSucceeeds() { var sem = new TestServiceEndpointManager(new ServiceEndpoint(ConnectionString1)); - var throws = new ServiceConnectionNotActiveException(); - var router = new TestEndpointRouter(throws); + var router = new TestEndpointRouter(); var writeTcs = new TaskCompletionSource(); TestServiceConnectionContainer innerContainer = null; @@ -176,12 +162,44 @@ public async Task TestContainerWithOneEndpointWithAllConnectedSucceeeds() await task.OrTimeout(); } + [Fact] + public async Task TestContainerWithOneEndpointWithAllDisconnectedConnectionThrows() + { + using (StartVerifiableLog(out var loggerFactory, LogLevel.Warning, logChecker: logs => + { + var warns = logs.Where(s => s.Write.LogLevel == LogLevel.Warning).ToList(); + Assert.Single(warns); + Assert.Contains(warns, s => s.Write.Message.Contains("Message JoinGroupWithAckMessage is not sent to endpoint (Primary)http://url1 because all connections to this endpoint are offline.")); + return true; + })) + { + var sem = new TestServiceEndpointManager(new ServiceEndpoint(ConnectionString1)); + var router = new DefaultEndpointRouter(); + + var container = new TestMultiEndpointServiceConnectionContainer("hub", + e => new TestServiceConnectionContainer(new List { + new TestSimpleServiceConnection(ServiceConnectionStatus.Disconnected), + new TestSimpleServiceConnection(ServiceConnectionStatus.Disconnected), + new TestSimpleServiceConnection(ServiceConnectionStatus.Disconnected), + new TestSimpleServiceConnection(ServiceConnectionStatus.Disconnected), + new TestSimpleServiceConnection(ServiceConnectionStatus.Disconnected), + new TestSimpleServiceConnection(ServiceConnectionStatus.Disconnected), + new TestSimpleServiceConnection(ServiceConnectionStatus.Disconnected), + }, e), sem, router, loggerFactory); + + // All the connections started + _ = container.StartAsync(); + await container.ConnectionInitializedTask.OrTimeout(); + + await container.WriteAsync(DefaultGroupMessage); + } + } [Fact] public async Task TestContainerWithBadRouterThrows() { var sem = new TestServiceEndpointManager(new ServiceEndpoint(ConnectionString1)); - var throws = new ServiceConnectionNotActiveException(); + var throws = new InvalidOperationException(); var router = new TestEndpointRouter(throws); var container = new TestMultiEndpointServiceConnectionContainer("hub", e => new TestServiceConnectionContainer(new List { @@ -210,7 +228,7 @@ public async Task TestContainerWithTwoConnectedEndpointAndBadRouterThrows() new ServiceEndpoint(ConnectionString1), new ServiceEndpoint(ConnectionString2)); - var router = new TestEndpointRouter(new ServiceConnectionNotActiveException()); + var router = new TestEndpointRouter(new InvalidOperationException()); var container = new TestMultiEndpointServiceConnectionContainer("hub", e => new TestServiceConnectionContainer(new List { new TestSimpleServiceConnection(), @@ -226,7 +244,7 @@ public async Task TestContainerWithTwoConnectedEndpointAndBadRouterThrows() _ = container.StartAsync(); await container.ConnectionInitializedTask; - await Assert.ThrowsAsync( + await Assert.ThrowsAsync( () => container.WriteAsync(DefaultGroupMessage) ); } @@ -705,6 +723,29 @@ public async Task TestTwoEndpointsWithCancellationToken() await Assert.ThrowsAnyAsync(async () => await task).OrTimeout(); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task TestSingleEndpointOffline(bool migratable) + { + var manager = new TestServiceEndpointManager( + new ServiceEndpoint(ConnectionString1) + ); + await TestEndpointOfflineInner(manager, new TestEndpointRouter(), migratable); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task TestMultiEndpointOffline(bool migratable) + { + var manager = new TestServiceEndpointManager( + new ServiceEndpoint(ConnectionString1), + new ServiceEndpoint(ConnectionString2) + ); + await TestEndpointOfflineInner(manager, new TestEndpointRouter(), migratable); + } + private async Task TestEndpointOfflineInner(IServiceEndpointManager manager, IEndpointRouter router, bool migratable) { var containers = new List(); @@ -740,29 +781,6 @@ private async Task TestEndpointOfflineInner(IServiceEndpointManager manager, IEn } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task TestSingleEndpointOffline(bool migratable) - { - var manager = new TestServiceEndpointManager( - new ServiceEndpoint(ConnectionString1) - ); - await TestEndpointOfflineInner(manager, null, migratable); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task TestMultiEndpointOffline(bool migratable) - { - var manager = new TestServiceEndpointManager( - new ServiceEndpoint(ConnectionString1), - new ServiceEndpoint(ConnectionString2) - ); - await TestEndpointOfflineInner(manager, new TestEndpointRouter(), migratable); - } - private class NotExistEndpointRouter : EndpointRouterDecorator { public override IEnumerable GetEndpointsForConnection(string connectionId, IEnumerable endpoints) @@ -774,6 +792,19 @@ public override IEnumerable GetEndpointsForGroup(string groupNa { return null; } + } + + private sealed class TestMultiEndpointServiceConnectionContainer : MultiEndpointServiceConnectionContainer + { + public TestMultiEndpointServiceConnectionContainer(string hub, + Func generator, + IServiceEndpointManager endpoint, + IEndpointRouter router, + ILoggerFactory loggerFactory + + ) : base(hub, generator, endpoint, router, loggerFactory) + { + } } private class TestServiceEndpointManager : ServiceEndpointManagerBase From a2d9f079c3533499e6a88f5ac40d7ad44d040c2f Mon Sep 17 00:00:00 2001 From: "Liangying.Wei" Date: Fri, 21 Feb 2020 14:30:31 +0800 Subject: [PATCH 03/14] =?UTF-8?q?Change=20netcore31=20chatsample=20to=20de?= =?UTF-8?q?fault=20chatsample=20and=20add=20messagepack=E2=80=A6=20(#829)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Change netcore31 chatsample to default chatsample and add messagepack dep * Update * Fix build failure --- AzureSignalR.sln | 28 ++++---- build/dependencies.props | 3 +- .../ChatSample.CSharpClient.csproj | 3 +- .../ChatSample.CSharpClient/Program.cs | 6 +- .../ChatSample/ChatSample.CoreApp3/Program.cs | 20 ------ .../ChatSample/ChatSample.CoreApp3/Startup.cs | 36 ---------- .../ChatSample.CoreApp3/appsettings.json | 15 ---- .../ChatSample.NetCore21.csproj} | 9 ++- .../Dockerfile | 6 +- .../Hub/BenchHub.cs | 0 .../Hub/Chat.cs | 10 +-- .../LoggerExtension.cs | 0 .../ChatSample.NetCore21/Program.cs | 27 +++++++ .../Properties/launchSettings.json | 13 ++-- .../ChatSample.NetCore21/Startup.cs | 35 +++++++++ .../ChatSample.NetCore21/appsettings.json | 13 ++++ .../package-lock.json | 0 .../package.json | 0 .../wwwroot/css/bootstrap.css | 0 .../wwwroot/css/site.css | 8 +++ .../wwwroot/favicon.ico | Bin .../wwwroot/index.html | 60 ++++++++++------ .../wwwroot/scripts/bootstrap.js | 0 .../wwwroot/scripts/bootstrap.min.js | 0 .../wwwroot/scripts/jquery-1.10.2.js | 0 .../wwwroot/scripts/jquery-1.10.2.min.js | 0 .../wwwroot/scripts/msgpack5.js | 0 .../wwwroot/scripts/msgpack5.min.js | 0 .../scripts/signalr-protocol-msgpack.js | 0 .../scripts/signalr-protocol-msgpack.js.map | 0 .../scripts/signalr-protocol-msgpack.min.js | 0 .../signalr-protocol-msgpack.min.js.map | 0 .../wwwroot/scripts/signalr.js | 0 .../wwwroot/scripts/signalr.js.map | 0 .../wwwroot/scripts/signalr.min.js | 0 .../wwwroot/scripts/signalr.min.js.map | 0 .../Chat.cs | 4 +- .../ChatSample/ChatSample/ChatSample.csproj | 6 +- samples/ChatSample/ChatSample/Dockerfile | 6 +- samples/ChatSample/ChatSample/Program.cs | 23 +++--- .../ChatSample/Properties/launchSettings.json | 11 +-- samples/ChatSample/ChatSample/Startup.cs | 68 +++++++++--------- .../appsettings.Development.json | 0 .../ChatSample/ChatSample/appsettings.json | 10 +-- .../ChatSample/wwwroot/css/site.css | 8 --- .../ChatSample/ChatSample/wwwroot/index.html | 61 ++++++---------- 46 files changed, 248 insertions(+), 241 deletions(-) delete mode 100644 samples/ChatSample/ChatSample.CoreApp3/Program.cs delete mode 100644 samples/ChatSample/ChatSample.CoreApp3/Startup.cs delete mode 100644 samples/ChatSample/ChatSample.CoreApp3/appsettings.json rename samples/ChatSample/{ChatSample.CoreApp3/ChatSample.CoreApp3.csproj => ChatSample.NetCore21/ChatSample.NetCore21.csproj} (69%) rename samples/ChatSample/{ChatSample.CoreApp3 => ChatSample.NetCore21}/Dockerfile (76%) rename samples/ChatSample/{ChatSample => ChatSample.NetCore21}/Hub/BenchHub.cs (100%) rename samples/ChatSample/{ChatSample => ChatSample.NetCore21}/Hub/Chat.cs (99%) rename samples/ChatSample/{ChatSample => ChatSample.NetCore21}/LoggerExtension.cs (100%) create mode 100644 samples/ChatSample/ChatSample.NetCore21/Program.cs rename samples/ChatSample/{ChatSample.CoreApp3 => ChatSample.NetCore21}/Properties/launchSettings.json (69%) create mode 100644 samples/ChatSample/ChatSample.NetCore21/Startup.cs create mode 100644 samples/ChatSample/ChatSample.NetCore21/appsettings.json rename samples/ChatSample/{ChatSample => ChatSample.NetCore21}/package-lock.json (100%) rename samples/ChatSample/{ChatSample => ChatSample.NetCore21}/package.json (100%) rename samples/ChatSample/{ChatSample.CoreApp3 => ChatSample.NetCore21}/wwwroot/css/bootstrap.css (100%) rename samples/ChatSample/{ChatSample.CoreApp3 => ChatSample.NetCore21}/wwwroot/css/site.css (96%) rename samples/ChatSample/{ChatSample.CoreApp3 => ChatSample.NetCore21}/wwwroot/favicon.ico (100%) rename samples/ChatSample/{ChatSample.CoreApp3 => ChatSample.NetCore21}/wwwroot/index.html (80%) rename samples/ChatSample/{ChatSample.CoreApp3 => ChatSample.NetCore21}/wwwroot/scripts/bootstrap.js (100%) rename samples/ChatSample/{ChatSample.CoreApp3 => ChatSample.NetCore21}/wwwroot/scripts/bootstrap.min.js (100%) rename samples/ChatSample/{ChatSample.CoreApp3 => ChatSample.NetCore21}/wwwroot/scripts/jquery-1.10.2.js (100%) rename samples/ChatSample/{ChatSample.CoreApp3 => ChatSample.NetCore21}/wwwroot/scripts/jquery-1.10.2.min.js (100%) rename samples/ChatSample/{ChatSample.CoreApp3 => ChatSample.NetCore21}/wwwroot/scripts/msgpack5.js (100%) rename samples/ChatSample/{ChatSample.CoreApp3 => ChatSample.NetCore21}/wwwroot/scripts/msgpack5.min.js (100%) rename samples/ChatSample/{ChatSample.CoreApp3 => ChatSample.NetCore21}/wwwroot/scripts/signalr-protocol-msgpack.js (100%) rename samples/ChatSample/{ChatSample.CoreApp3 => ChatSample.NetCore21}/wwwroot/scripts/signalr-protocol-msgpack.js.map (100%) rename samples/ChatSample/{ChatSample.CoreApp3 => ChatSample.NetCore21}/wwwroot/scripts/signalr-protocol-msgpack.min.js (100%) rename samples/ChatSample/{ChatSample.CoreApp3 => ChatSample.NetCore21}/wwwroot/scripts/signalr-protocol-msgpack.min.js.map (100%) rename samples/ChatSample/{ChatSample.CoreApp3 => ChatSample.NetCore21}/wwwroot/scripts/signalr.js (100%) rename samples/ChatSample/{ChatSample.CoreApp3 => ChatSample.NetCore21}/wwwroot/scripts/signalr.js.map (100%) rename samples/ChatSample/{ChatSample.CoreApp3 => ChatSample.NetCore21}/wwwroot/scripts/signalr.min.js (100%) rename samples/ChatSample/{ChatSample.CoreApp3 => ChatSample.NetCore21}/wwwroot/scripts/signalr.min.js.map (100%) rename samples/ChatSample/{ChatSample.CoreApp3 => ChatSample}/Chat.cs (99%) rename samples/ChatSample/{ChatSample.CoreApp3 => ChatSample}/appsettings.Development.json (100%) diff --git a/AzureSignalR.sln b/AzureSignalR.sln index 24978da5a..aebc44425 100644 --- a/AzureSignalR.sln +++ b/AzureSignalR.sln @@ -54,20 +54,20 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Azure.SignalR.Man EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ChatSample", "ChatSample", "{C965ED06-6A17-4329-B3C6-811830F4F4ED}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChatSample", "samples\ChatSample\ChatSample\ChatSample.csproj", "{3F9B3FD8-A54F-4AB4-B9D4-D4D55686A0E9}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChatSample.CSharpClient", "samples\ChatSample\ChatSample.CSharpClient\ChatSample.CSharpClient.csproj", "{C51AA61E-5210-4545-B49E-61255C136B83}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Azure.SignalR.Tests.Common", "test\Microsoft.Azure.SignalR.Tests.Common\Microsoft.Azure.SignalR.Tests.Common.csproj", "{981419D7-5199-4829-ABE2-421D05EF7D8C}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChatSample.CoreApp3", "samples\ChatSample\ChatSample.CoreApp3\ChatSample.CoreApp3.csproj", "{1F6D9B7A-36A0-4B71-825B-77F832B425A5}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Azure.SignalR.E2ETests", "test\Microsoft.Azure.SignalR.E2ETests\Microsoft.Azure.SignalR.E2ETests.csproj", "{B8BA8F89-8028-4691-82C6-8F9E946B6B22}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Azure.SignalR.AspNet.E2ETests", "test\Microsoft.Azure.SignalR.AspNet.E2ETests\Microsoft.Azure.SignalR.AspNet.E2ETests.csproj", "{5D97A2CB-DCD6-4865-9C39-34687A14AAE1}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorAppSample", "samples\BlazorAppSample\BlazorAppSample.csproj", "{CDE3C0B5-CD0B-42DE-9CE1-638266A2A1AE}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChatSample", "samples\ChatSample\ChatSample\ChatSample.csproj", "{63BB9836-126B-4E65-92B9-019438185E41}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChatSample.NetCore21", "samples\ChatSample\ChatSample.NetCore21\ChatSample.NetCore21.csproj", "{1D06C79C-4E97-40F6-A860-092941922EFA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -126,10 +126,6 @@ Global {493CE126-D147-434E-ACF8-675E89D0A981}.Debug|Any CPU.Build.0 = Debug|Any CPU {493CE126-D147-434E-ACF8-675E89D0A981}.Release|Any CPU.ActiveCfg = Release|Any CPU {493CE126-D147-434E-ACF8-675E89D0A981}.Release|Any CPU.Build.0 = Release|Any CPU - {3F9B3FD8-A54F-4AB4-B9D4-D4D55686A0E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3F9B3FD8-A54F-4AB4-B9D4-D4D55686A0E9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3F9B3FD8-A54F-4AB4-B9D4-D4D55686A0E9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3F9B3FD8-A54F-4AB4-B9D4-D4D55686A0E9}.Release|Any CPU.Build.0 = Release|Any CPU {C51AA61E-5210-4545-B49E-61255C136B83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C51AA61E-5210-4545-B49E-61255C136B83}.Debug|Any CPU.Build.0 = Debug|Any CPU {C51AA61E-5210-4545-B49E-61255C136B83}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -138,10 +134,6 @@ Global {981419D7-5199-4829-ABE2-421D05EF7D8C}.Debug|Any CPU.Build.0 = Debug|Any CPU {981419D7-5199-4829-ABE2-421D05EF7D8C}.Release|Any CPU.ActiveCfg = Release|Any CPU {981419D7-5199-4829-ABE2-421D05EF7D8C}.Release|Any CPU.Build.0 = Release|Any CPU - {1F6D9B7A-36A0-4B71-825B-77F832B425A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1F6D9B7A-36A0-4B71-825B-77F832B425A5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1F6D9B7A-36A0-4B71-825B-77F832B425A5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1F6D9B7A-36A0-4B71-825B-77F832B425A5}.Release|Any CPU.Build.0 = Release|Any CPU {B8BA8F89-8028-4691-82C6-8F9E946B6B22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B8BA8F89-8028-4691-82C6-8F9E946B6B22}.Debug|Any CPU.Build.0 = Debug|Any CPU {B8BA8F89-8028-4691-82C6-8F9E946B6B22}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -154,6 +146,14 @@ Global {CDE3C0B5-CD0B-42DE-9CE1-638266A2A1AE}.Debug|Any CPU.Build.0 = Debug|Any CPU {CDE3C0B5-CD0B-42DE-9CE1-638266A2A1AE}.Release|Any CPU.ActiveCfg = Release|Any CPU {CDE3C0B5-CD0B-42DE-9CE1-638266A2A1AE}.Release|Any CPU.Build.0 = Release|Any CPU + {63BB9836-126B-4E65-92B9-019438185E41}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {63BB9836-126B-4E65-92B9-019438185E41}.Debug|Any CPU.Build.0 = Debug|Any CPU + {63BB9836-126B-4E65-92B9-019438185E41}.Release|Any CPU.ActiveCfg = Release|Any CPU + {63BB9836-126B-4E65-92B9-019438185E41}.Release|Any CPU.Build.0 = Release|Any CPU + {1D06C79C-4E97-40F6-A860-092941922EFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1D06C79C-4E97-40F6-A860-092941922EFA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1D06C79C-4E97-40F6-A860-092941922EFA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1D06C79C-4E97-40F6-A860-092941922EFA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -174,13 +174,13 @@ Global {8DC103BA-7404-479F-BF18-90EF3EC3CEA9} = {DA69F624-5398-4884-87E4-B816698CDE65} {493CE126-D147-434E-ACF8-675E89D0A981} = {2429FBD8-1FCE-4C42-AA28-DF32F7249E77} {C965ED06-6A17-4329-B3C6-811830F4F4ED} = {C4BC9889-B49F-41B6-806B-F84941B2549B} - {3F9B3FD8-A54F-4AB4-B9D4-D4D55686A0E9} = {C965ED06-6A17-4329-B3C6-811830F4F4ED} {C51AA61E-5210-4545-B49E-61255C136B83} = {C965ED06-6A17-4329-B3C6-811830F4F4ED} {981419D7-5199-4829-ABE2-421D05EF7D8C} = {2429FBD8-1FCE-4C42-AA28-DF32F7249E77} - {1F6D9B7A-36A0-4B71-825B-77F832B425A5} = {C965ED06-6A17-4329-B3C6-811830F4F4ED} {B8BA8F89-8028-4691-82C6-8F9E946B6B22} = {2429FBD8-1FCE-4C42-AA28-DF32F7249E77} {5D97A2CB-DCD6-4865-9C39-34687A14AAE1} = {2429FBD8-1FCE-4C42-AA28-DF32F7249E77} {CDE3C0B5-CD0B-42DE-9CE1-638266A2A1AE} = {C4BC9889-B49F-41B6-806B-F84941B2549B} + {63BB9836-126B-4E65-92B9-019438185E41} = {C965ED06-6A17-4329-B3C6-811830F4F4ED} + {1D06C79C-4E97-40F6-A860-092941922EFA} = {C965ED06-6A17-4329-B3C6-811830F4F4ED} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7945A4E4-ACDB-4F6E-95CA-6AC6E7C2CD59} diff --git a/build/dependencies.props b/build/dependencies.props index c76addbe9..17f8cd365 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -42,7 +42,8 @@ 2.1.0 - 1.1.0 + 3.1.2 + 3.1.2 11.0.2 2.1.0 1.0.0 diff --git a/samples/ChatSample/ChatSample.CSharpClient/ChatSample.CSharpClient.csproj b/samples/ChatSample/ChatSample.CSharpClient/ChatSample.CSharpClient.csproj index 9bd3f5a8f..b3b79c320 100644 --- a/samples/ChatSample/ChatSample.CSharpClient/ChatSample.CSharpClient.csproj +++ b/samples/ChatSample/ChatSample.CSharpClient/ChatSample.CSharpClient.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp2.1 + netcoreapp3.0 @@ -11,6 +11,7 @@ + diff --git a/samples/ChatSample/ChatSample.CSharpClient/Program.cs b/samples/ChatSample/ChatSample.CSharpClient/Program.cs index a15e4b402..d232efb1c 100644 --- a/samples/ChatSample/ChatSample.CSharpClient/Program.cs +++ b/samples/ChatSample/ChatSample.CSharpClient/Program.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Azure.SignalR; +using Microsoft.Extensions.DependencyInjection; namespace ChatSample.CSharpClient { @@ -29,7 +30,6 @@ static async Task Main(string[] args) { case Mode.Broadcast: await proxy.InvokeAsync("BroadcastMessage", currentUser, input); - break; case Mode.Echo: await proxy.InvokeAsync("echo", input); @@ -44,7 +44,9 @@ static async Task Main(string[] args) private static async Task ConnectAsync(string url, TextWriter output, CancellationToken cancellationToken = default) { - var connection = new HubConnectionBuilder().WithUrl(url).Build(); + var connection = new HubConnectionBuilder() + .WithUrl(url) + .AddMessagePackProtocol().Build(); connection.On("BroadcastMessage", BroadcastMessage); connection.On("Echo", Echo); diff --git a/samples/ChatSample/ChatSample.CoreApp3/Program.cs b/samples/ChatSample/ChatSample.CoreApp3/Program.cs deleted file mode 100644 index c9e3bf2b6..000000000 --- a/samples/ChatSample/ChatSample.CoreApp3/Program.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Hosting; - -namespace ChatSample.CoreApp3 -{ - public class Program - { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); - } -} diff --git a/samples/ChatSample/ChatSample.CoreApp3/Startup.cs b/samples/ChatSample/ChatSample.CoreApp3/Startup.cs deleted file mode 100644 index f0cacd7d5..000000000 --- a/samples/ChatSample/ChatSample.CoreApp3/Startup.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace ChatSample.CoreApp3 -{ - public class Startup - { - // This method gets called by the runtime. Use this method to add services to the container. - // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 - public void ConfigureServices(IServiceCollection services) - { - services.AddMvc(); - services.AddSignalR(); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - app.UseFileServer(); - app.UseRouting(); - app.UseAuthorization(); - - app.UseEndpoints(routes => - { - routes.MapHub("/chat"); - routes.MapHub("/notificatons"); - }); - } - } -} diff --git a/samples/ChatSample/ChatSample.CoreApp3/appsettings.json b/samples/ChatSample/ChatSample.CoreApp3/appsettings.json deleted file mode 100644 index bee26f720..000000000 --- a/samples/ChatSample/ChatSample.CoreApp3/appsettings.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "Azure": { - "SignalR": { - "Enabled": true, - "ConnectionString": "" - } - }, - "Logging": { - "LogLevel": { - "Default": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - }, - "AllowedHosts": "*" -} diff --git a/samples/ChatSample/ChatSample.CoreApp3/ChatSample.CoreApp3.csproj b/samples/ChatSample/ChatSample.NetCore21/ChatSample.NetCore21.csproj similarity index 69% rename from samples/ChatSample/ChatSample.CoreApp3/ChatSample.CoreApp3.csproj rename to samples/ChatSample/ChatSample.NetCore21/ChatSample.NetCore21.csproj index 6871bfc4e..757eca997 100644 --- a/samples/ChatSample/ChatSample.CoreApp3/ChatSample.CoreApp3.csproj +++ b/samples/ChatSample/ChatSample.NetCore21/ChatSample.NetCore21.csproj @@ -1,10 +1,13 @@  - netcoreapp3.0 + netcoreapp2.1 + chatsample - - + + + + diff --git a/samples/ChatSample/ChatSample.CoreApp3/Dockerfile b/samples/ChatSample/ChatSample.NetCore21/Dockerfile similarity index 76% rename from samples/ChatSample/ChatSample.CoreApp3/Dockerfile rename to samples/ChatSample/ChatSample.NetCore21/Dockerfile index baafe7619..f0e0745e2 100644 --- a/samples/ChatSample/ChatSample.CoreApp3/Dockerfile +++ b/samples/ChatSample/ChatSample.NetCore21/Dockerfile @@ -6,7 +6,7 @@ FROM mcr.microsoft.com/dotnet/core/sdk:3.0 COPY ./ /home/signalr-src -RUN cd /home/signalr-src/samples/ChatSample/ChatSample.CoreApp3 && \ +RUN cd /home/signalr-src/samples/ChatSample/ChatSample && \ dotnet build && \ dotnet publish -r ubuntu.16.04-x64 -c Release -o /home/build/ @@ -14,7 +14,7 @@ RUN cd /home/signalr-src/samples/ChatSample/ChatSample.CoreApp3 && \ # Final stage ################################################## -FROM mcr.microsoft.com/dotnet/core/runtime:3.0 +FROM mcr.microsoft.com/dotnet/core/runtime:2.1 COPY --from=0 /home/build/ /home/SignalR @@ -22,4 +22,4 @@ WORKDIR /home/SignalR EXPOSE 5050 -CMD ["./ChatSample.CoreApp3"] +CMD ["./ChatSample"] diff --git a/samples/ChatSample/ChatSample/Hub/BenchHub.cs b/samples/ChatSample/ChatSample.NetCore21/Hub/BenchHub.cs similarity index 100% rename from samples/ChatSample/ChatSample/Hub/BenchHub.cs rename to samples/ChatSample/ChatSample.NetCore21/Hub/BenchHub.cs diff --git a/samples/ChatSample/ChatSample/Hub/Chat.cs b/samples/ChatSample/ChatSample.NetCore21/Hub/Chat.cs similarity index 99% rename from samples/ChatSample/ChatSample/Hub/Chat.cs rename to samples/ChatSample/ChatSample.NetCore21/Hub/Chat.cs index a1bd24400..3516869b0 100644 --- a/samples/ChatSample/ChatSample/Hub/Chat.cs +++ b/samples/ChatSample/ChatSample.NetCore21/Hub/Chat.cs @@ -2,7 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; -using System.Threading.Tasks; +using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR; namespace ChatSample @@ -21,10 +21,10 @@ public void Echo(string name, string message) Console.WriteLine("Echo..."); } - public override async Task OnConnectedAsync() - { - Console.WriteLine($"{Context.ConnectionId} connected."); - await base.OnConnectedAsync(); + public override async Task OnConnectedAsync() + { + Console.WriteLine($"{Context.ConnectionId} connected."); + await base.OnConnectedAsync(); } public override async Task OnDisconnectedAsync(Exception e) diff --git a/samples/ChatSample/ChatSample/LoggerExtension.cs b/samples/ChatSample/ChatSample.NetCore21/LoggerExtension.cs similarity index 100% rename from samples/ChatSample/ChatSample/LoggerExtension.cs rename to samples/ChatSample/ChatSample.NetCore21/LoggerExtension.cs diff --git a/samples/ChatSample/ChatSample.NetCore21/Program.cs b/samples/ChatSample/ChatSample.NetCore21/Program.cs new file mode 100644 index 000000000..134652694 --- /dev/null +++ b/samples/ChatSample/ChatSample.NetCore21/Program.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Logging; + +namespace ChatSample +{ + public class Program + { + public static void Main(string[] args) + { + CreateWebHostBuilder(args).Build().Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => + WebHost.CreateDefaultBuilder(args) + .ConfigureLogging( + (hostingContext, logging) => + { + logging.AddTimedConsole(); + logging.AddDebug(); + }) + .UseStartup(); + } +} diff --git a/samples/ChatSample/ChatSample.CoreApp3/Properties/launchSettings.json b/samples/ChatSample/ChatSample.NetCore21/Properties/launchSettings.json similarity index 69% rename from samples/ChatSample/ChatSample.CoreApp3/Properties/launchSettings.json rename to samples/ChatSample/ChatSample.NetCore21/Properties/launchSettings.json index 39557efa4..3d15ed165 100644 --- a/samples/ChatSample/ChatSample.CoreApp3/Properties/launchSettings.json +++ b/samples/ChatSample/ChatSample.NetCore21/Properties/launchSettings.json @@ -1,7 +1,7 @@ -{ +{ "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, + "windowsAuthentication": false, + "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:8080", "sslPort": 0 @@ -15,13 +15,12 @@ "ASPNETCORE_ENVIRONMENT": "Development" } }, - "ChatSample.Core3": { + "ChatSample": { "commandName": "Project", "launchBrowser": true, - "applicationUrl": "https://localhost:5050", + "applicationUrl": "http://localhost:5050", "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.Azure.SignalR" + "ASPNETCORE_ENVIRONMENT": "Development" } }, "ServerOnly": { diff --git a/samples/ChatSample/ChatSample.NetCore21/Startup.cs b/samples/ChatSample/ChatSample.NetCore21/Startup.cs new file mode 100644 index 000000000..b3f6de5fe --- /dev/null +++ b/samples/ChatSample/ChatSample.NetCore21/Startup.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace ChatSample +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + public void ConfigureServices(IServiceCollection services) + { + services.AddSignalR() + .AddAzureSignalR(); + } + + public void Configure(IApplicationBuilder app) + { + app.UseFileServer(); + app.UseAzureSignalR(routes => + { + routes.MapHub("/chat"); + routes.MapHub("/signalrbench"); + }); + } + } +} diff --git a/samples/ChatSample/ChatSample.NetCore21/appsettings.json b/samples/ChatSample/ChatSample.NetCore21/appsettings.json new file mode 100644 index 000000000..dc9c29a5c --- /dev/null +++ b/samples/ChatSample/ChatSample.NetCore21/appsettings.json @@ -0,0 +1,13 @@ +{ + "Azure": { + "SignalR": { + "ConnectionString": "" + } + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Azure.SignalR": "Information" + } + } +} diff --git a/samples/ChatSample/ChatSample/package-lock.json b/samples/ChatSample/ChatSample.NetCore21/package-lock.json similarity index 100% rename from samples/ChatSample/ChatSample/package-lock.json rename to samples/ChatSample/ChatSample.NetCore21/package-lock.json diff --git a/samples/ChatSample/ChatSample/package.json b/samples/ChatSample/ChatSample.NetCore21/package.json similarity index 100% rename from samples/ChatSample/ChatSample/package.json rename to samples/ChatSample/ChatSample.NetCore21/package.json diff --git a/samples/ChatSample/ChatSample.CoreApp3/wwwroot/css/bootstrap.css b/samples/ChatSample/ChatSample.NetCore21/wwwroot/css/bootstrap.css similarity index 100% rename from samples/ChatSample/ChatSample.CoreApp3/wwwroot/css/bootstrap.css rename to samples/ChatSample/ChatSample.NetCore21/wwwroot/css/bootstrap.css diff --git a/samples/ChatSample/ChatSample.CoreApp3/wwwroot/css/site.css b/samples/ChatSample/ChatSample.NetCore21/wwwroot/css/site.css similarity index 96% rename from samples/ChatSample/ChatSample.CoreApp3/wwwroot/css/site.css rename to samples/ChatSample/ChatSample.NetCore21/wwwroot/css/site.css index 1aa124ac0..4453ee2b0 100644 --- a/samples/ChatSample/ChatSample.CoreApp3/wwwroot/css/site.css +++ b/samples/ChatSample/ChatSample.NetCore21/wwwroot/css/site.css @@ -31,6 +31,14 @@ textarea:focus { background: #87CEFA; } +.error { + color: red +} + +.success { + color: green +} + .broadcast-message { display: inline-block; background: yellow; diff --git a/samples/ChatSample/ChatSample.CoreApp3/wwwroot/favicon.ico b/samples/ChatSample/ChatSample.NetCore21/wwwroot/favicon.ico similarity index 100% rename from samples/ChatSample/ChatSample.CoreApp3/wwwroot/favicon.ico rename to samples/ChatSample/ChatSample.NetCore21/wwwroot/favicon.ico diff --git a/samples/ChatSample/ChatSample.CoreApp3/wwwroot/index.html b/samples/ChatSample/ChatSample.NetCore21/wwwroot/index.html similarity index 80% rename from samples/ChatSample/ChatSample.CoreApp3/wwwroot/index.html rename to samples/ChatSample/ChatSample.NetCore21/wwwroot/index.html index afca852e9..b6cf568f0 100644 --- a/samples/ChatSample/ChatSample.CoreApp3/wwwroot/index.html +++ b/samples/ChatSample/ChatSample.NetCore21/wwwroot/index.html @@ -11,6 +11,7 @@

Azure SignalR Group Chat

+
@@ -85,17 +86,25 @@

/g, ">"); + } + + function displayMessage(name, message) { + var encodedMsg = encodeMessage(message); + var messageEntry = createMessageEntry(name, encodedMsg); + + var messageBox = document.getElementById('messages'); + messageBox.appendChild(messageEntry); + messageBox.scrollTop = messageBox.scrollHeight; + } + function bindConnectionMessage(connection) { var messageCallback = function (name, message) { if (!message) return; // Html encode display name and message. - var encodedName = name; - var encodedMsg = message.replace(/&/g, "&").replace(//g, ">"); - var messageEntry = createMessageEntry(encodedName, encodedMsg); - - var messageBox = document.getElementById('messages'); - messageBox.appendChild(messageEntry); - messageBox.scrollTop = messageBox.scrollHeight; + displayMessage(name, message); }; // Create a function that the hub can call to broadcast messages. connection.on('broadcastMessage', messageCallback); @@ -135,27 +144,38 @@

Connect error: " + encodeMessage(errorMessage) + ". Reconnecting...

") + setTimeout(function () { + startConnectionWithRetry(connection) + }, getRandom(200, 2000)); + } + + function startConnectionWithRetry(hubConnection) { + hubConnection.start() + .then(function () { + $('#status').html("
Connected.
") + onConnected(hubConnection); + }) + .catch(onConnectionError); } var connection = new signalR.HubConnectionBuilder() - .withUrl('/chat') - .build(); - + .withUrl('/chat') + .build(); + bindConnectionMessage(connection); - connection.start() - .then(function () { - onConnected(connection); - }) - .catch(function (error) { - console.error(error.message); - }); + startConnectionWithRetry(connection); }); diff --git a/samples/ChatSample/ChatSample.CoreApp3/wwwroot/scripts/bootstrap.js b/samples/ChatSample/ChatSample.NetCore21/wwwroot/scripts/bootstrap.js similarity index 100% rename from samples/ChatSample/ChatSample.CoreApp3/wwwroot/scripts/bootstrap.js rename to samples/ChatSample/ChatSample.NetCore21/wwwroot/scripts/bootstrap.js diff --git a/samples/ChatSample/ChatSample.CoreApp3/wwwroot/scripts/bootstrap.min.js b/samples/ChatSample/ChatSample.NetCore21/wwwroot/scripts/bootstrap.min.js similarity index 100% rename from samples/ChatSample/ChatSample.CoreApp3/wwwroot/scripts/bootstrap.min.js rename to samples/ChatSample/ChatSample.NetCore21/wwwroot/scripts/bootstrap.min.js diff --git a/samples/ChatSample/ChatSample.CoreApp3/wwwroot/scripts/jquery-1.10.2.js b/samples/ChatSample/ChatSample.NetCore21/wwwroot/scripts/jquery-1.10.2.js similarity index 100% rename from samples/ChatSample/ChatSample.CoreApp3/wwwroot/scripts/jquery-1.10.2.js rename to samples/ChatSample/ChatSample.NetCore21/wwwroot/scripts/jquery-1.10.2.js diff --git a/samples/ChatSample/ChatSample.CoreApp3/wwwroot/scripts/jquery-1.10.2.min.js b/samples/ChatSample/ChatSample.NetCore21/wwwroot/scripts/jquery-1.10.2.min.js similarity index 100% rename from samples/ChatSample/ChatSample.CoreApp3/wwwroot/scripts/jquery-1.10.2.min.js rename to samples/ChatSample/ChatSample.NetCore21/wwwroot/scripts/jquery-1.10.2.min.js diff --git a/samples/ChatSample/ChatSample.CoreApp3/wwwroot/scripts/msgpack5.js b/samples/ChatSample/ChatSample.NetCore21/wwwroot/scripts/msgpack5.js similarity index 100% rename from samples/ChatSample/ChatSample.CoreApp3/wwwroot/scripts/msgpack5.js rename to samples/ChatSample/ChatSample.NetCore21/wwwroot/scripts/msgpack5.js diff --git a/samples/ChatSample/ChatSample.CoreApp3/wwwroot/scripts/msgpack5.min.js b/samples/ChatSample/ChatSample.NetCore21/wwwroot/scripts/msgpack5.min.js similarity index 100% rename from samples/ChatSample/ChatSample.CoreApp3/wwwroot/scripts/msgpack5.min.js rename to samples/ChatSample/ChatSample.NetCore21/wwwroot/scripts/msgpack5.min.js diff --git a/samples/ChatSample/ChatSample.CoreApp3/wwwroot/scripts/signalr-protocol-msgpack.js b/samples/ChatSample/ChatSample.NetCore21/wwwroot/scripts/signalr-protocol-msgpack.js similarity index 100% rename from samples/ChatSample/ChatSample.CoreApp3/wwwroot/scripts/signalr-protocol-msgpack.js rename to samples/ChatSample/ChatSample.NetCore21/wwwroot/scripts/signalr-protocol-msgpack.js diff --git a/samples/ChatSample/ChatSample.CoreApp3/wwwroot/scripts/signalr-protocol-msgpack.js.map b/samples/ChatSample/ChatSample.NetCore21/wwwroot/scripts/signalr-protocol-msgpack.js.map similarity index 100% rename from samples/ChatSample/ChatSample.CoreApp3/wwwroot/scripts/signalr-protocol-msgpack.js.map rename to samples/ChatSample/ChatSample.NetCore21/wwwroot/scripts/signalr-protocol-msgpack.js.map diff --git a/samples/ChatSample/ChatSample.CoreApp3/wwwroot/scripts/signalr-protocol-msgpack.min.js b/samples/ChatSample/ChatSample.NetCore21/wwwroot/scripts/signalr-protocol-msgpack.min.js similarity index 100% rename from samples/ChatSample/ChatSample.CoreApp3/wwwroot/scripts/signalr-protocol-msgpack.min.js rename to samples/ChatSample/ChatSample.NetCore21/wwwroot/scripts/signalr-protocol-msgpack.min.js diff --git a/samples/ChatSample/ChatSample.CoreApp3/wwwroot/scripts/signalr-protocol-msgpack.min.js.map b/samples/ChatSample/ChatSample.NetCore21/wwwroot/scripts/signalr-protocol-msgpack.min.js.map similarity index 100% rename from samples/ChatSample/ChatSample.CoreApp3/wwwroot/scripts/signalr-protocol-msgpack.min.js.map rename to samples/ChatSample/ChatSample.NetCore21/wwwroot/scripts/signalr-protocol-msgpack.min.js.map diff --git a/samples/ChatSample/ChatSample.CoreApp3/wwwroot/scripts/signalr.js b/samples/ChatSample/ChatSample.NetCore21/wwwroot/scripts/signalr.js similarity index 100% rename from samples/ChatSample/ChatSample.CoreApp3/wwwroot/scripts/signalr.js rename to samples/ChatSample/ChatSample.NetCore21/wwwroot/scripts/signalr.js diff --git a/samples/ChatSample/ChatSample.CoreApp3/wwwroot/scripts/signalr.js.map b/samples/ChatSample/ChatSample.NetCore21/wwwroot/scripts/signalr.js.map similarity index 100% rename from samples/ChatSample/ChatSample.CoreApp3/wwwroot/scripts/signalr.js.map rename to samples/ChatSample/ChatSample.NetCore21/wwwroot/scripts/signalr.js.map diff --git a/samples/ChatSample/ChatSample.CoreApp3/wwwroot/scripts/signalr.min.js b/samples/ChatSample/ChatSample.NetCore21/wwwroot/scripts/signalr.min.js similarity index 100% rename from samples/ChatSample/ChatSample.CoreApp3/wwwroot/scripts/signalr.min.js rename to samples/ChatSample/ChatSample.NetCore21/wwwroot/scripts/signalr.min.js diff --git a/samples/ChatSample/ChatSample.CoreApp3/wwwroot/scripts/signalr.min.js.map b/samples/ChatSample/ChatSample.NetCore21/wwwroot/scripts/signalr.min.js.map similarity index 100% rename from samples/ChatSample/ChatSample.CoreApp3/wwwroot/scripts/signalr.min.js.map rename to samples/ChatSample/ChatSample.NetCore21/wwwroot/scripts/signalr.min.js.map diff --git a/samples/ChatSample/ChatSample.CoreApp3/Chat.cs b/samples/ChatSample/ChatSample/Chat.cs similarity index 99% rename from samples/ChatSample/ChatSample.CoreApp3/Chat.cs rename to samples/ChatSample/ChatSample/Chat.cs index f1a35ea1b..5c952d17d 100644 --- a/samples/ChatSample/ChatSample.CoreApp3/Chat.cs +++ b/samples/ChatSample/ChatSample/Chat.cs @@ -2,9 +2,9 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; -using System.Threading.Tasks; +using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR; - + namespace ChatSample.CoreApp3 { public class Chat : Hub diff --git a/samples/ChatSample/ChatSample/ChatSample.csproj b/samples/ChatSample/ChatSample/ChatSample.csproj index 757eca997..eb4137ac0 100644 --- a/samples/ChatSample/ChatSample/ChatSample.csproj +++ b/samples/ChatSample/ChatSample/ChatSample.csproj @@ -1,13 +1,11 @@  - netcoreapp2.1 + netcoreapp3.0 chatsample - - + - diff --git a/samples/ChatSample/ChatSample/Dockerfile b/samples/ChatSample/ChatSample/Dockerfile index f0e0745e2..baafe7619 100644 --- a/samples/ChatSample/ChatSample/Dockerfile +++ b/samples/ChatSample/ChatSample/Dockerfile @@ -6,7 +6,7 @@ FROM mcr.microsoft.com/dotnet/core/sdk:3.0 COPY ./ /home/signalr-src -RUN cd /home/signalr-src/samples/ChatSample/ChatSample && \ +RUN cd /home/signalr-src/samples/ChatSample/ChatSample.CoreApp3 && \ dotnet build && \ dotnet publish -r ubuntu.16.04-x64 -c Release -o /home/build/ @@ -14,7 +14,7 @@ RUN cd /home/signalr-src/samples/ChatSample/ChatSample && \ # Final stage ################################################## -FROM mcr.microsoft.com/dotnet/core/runtime:2.1 +FROM mcr.microsoft.com/dotnet/core/runtime:3.0 COPY --from=0 /home/build/ /home/SignalR @@ -22,4 +22,4 @@ WORKDIR /home/SignalR EXPOSE 5050 -CMD ["./ChatSample"] +CMD ["./ChatSample.CoreApp3"] diff --git a/samples/ChatSample/ChatSample/Program.cs b/samples/ChatSample/ChatSample/Program.cs index 134652694..c9e3bf2b6 100644 --- a/samples/ChatSample/ChatSample/Program.cs +++ b/samples/ChatSample/ChatSample/Program.cs @@ -1,27 +1,20 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Hosting; -namespace ChatSample +namespace ChatSample.CoreApp3 { public class Program { public static void Main(string[] args) { - CreateWebHostBuilder(args).Build().Run(); + CreateHostBuilder(args).Build().Run(); } - public static IWebHostBuilder CreateWebHostBuilder(string[] args) => - WebHost.CreateDefaultBuilder(args) - .ConfigureLogging( - (hostingContext, logging) => + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => { - logging.AddTimedConsole(); - logging.AddDebug(); - }) - .UseStartup(); + webBuilder.UseStartup(); + }); } } diff --git a/samples/ChatSample/ChatSample/Properties/launchSettings.json b/samples/ChatSample/ChatSample/Properties/launchSettings.json index 3d15ed165..70c36f99d 100644 --- a/samples/ChatSample/ChatSample/Properties/launchSettings.json +++ b/samples/ChatSample/ChatSample/Properties/launchSettings.json @@ -1,7 +1,7 @@ -{ +{ "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, + "windowsAuthentication": false, + "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:8080", "sslPort": 0 @@ -15,12 +15,13 @@ "ASPNETCORE_ENVIRONMENT": "Development" } }, - "ChatSample": { + "ChatSample.Core3": { "commandName": "Project", "launchBrowser": true, "applicationUrl": "http://localhost:5050", "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" + "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.Azure.SignalR" } }, "ServerOnly": { diff --git a/samples/ChatSample/ChatSample/Startup.cs b/samples/ChatSample/ChatSample/Startup.cs index 3cf7a5f43..034664953 100644 --- a/samples/ChatSample/ChatSample/Startup.cs +++ b/samples/ChatSample/ChatSample/Startup.cs @@ -1,35 +1,37 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; -namespace ChatSample -{ - public class Startup - { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - public void ConfigureServices(IServiceCollection services) - { - services.AddSignalR() - .AddAzureSignalR(); - } - - public void Configure(IApplicationBuilder app) +namespace ChatSample.CoreApp3 +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) { - app.UseFileServer(); - app.UseAzureSignalR(routes => - { - routes.MapHub("/chat"); - routes.MapHub("/signalrbench"); - }); - } - } -} + services.AddMvc(); + services.AddSignalR() + .AddMessagePackProtocol(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + app.UseFileServer(); + app.UseRouting(); + app.UseAuthorization(); + + app.UseEndpoints(routes => + { + routes.MapHub("/chat"); + routes.MapHub("/notificatons"); + }); + } + } +} diff --git a/samples/ChatSample/ChatSample.CoreApp3/appsettings.Development.json b/samples/ChatSample/ChatSample/appsettings.Development.json similarity index 100% rename from samples/ChatSample/ChatSample.CoreApp3/appsettings.Development.json rename to samples/ChatSample/ChatSample/appsettings.Development.json diff --git a/samples/ChatSample/ChatSample/appsettings.json b/samples/ChatSample/ChatSample/appsettings.json index dc9c29a5c..bee26f720 100644 --- a/samples/ChatSample/ChatSample/appsettings.json +++ b/samples/ChatSample/ChatSample/appsettings.json @@ -1,13 +1,15 @@ -{ +{ "Azure": { "SignalR": { + "Enabled": true, "ConnectionString": "" } }, "Logging": { "LogLevel": { - "Default": "Information", - "Microsoft.Azure.SignalR": "Information" + "Default": "Warning", + "Microsoft.Hosting.Lifetime": "Information" } - } + }, + "AllowedHosts": "*" } diff --git a/samples/ChatSample/ChatSample/wwwroot/css/site.css b/samples/ChatSample/ChatSample/wwwroot/css/site.css index 4453ee2b0..1aa124ac0 100644 --- a/samples/ChatSample/ChatSample/wwwroot/css/site.css +++ b/samples/ChatSample/ChatSample/wwwroot/css/site.css @@ -31,14 +31,6 @@ textarea:focus { background: #87CEFA; } -.error { - color: red -} - -.success { - color: green -} - .broadcast-message { display: inline-block; background: yellow; diff --git a/samples/ChatSample/ChatSample/wwwroot/index.html b/samples/ChatSample/ChatSample/wwwroot/index.html index b6cf568f0..9c507f3cf 100644 --- a/samples/ChatSample/ChatSample/wwwroot/index.html +++ b/samples/ChatSample/ChatSample/wwwroot/index.html @@ -11,7 +11,6 @@

Azure SignalR Group Chat

-
@@ -86,25 +85,17 @@

/g, ">"); - } - - function displayMessage(name, message) { - var encodedMsg = encodeMessage(message); - var messageEntry = createMessageEntry(name, encodedMsg); - - var messageBox = document.getElementById('messages'); - messageBox.appendChild(messageEntry); - messageBox.scrollTop = messageBox.scrollHeight; - } - function bindConnectionMessage(connection) { var messageCallback = function (name, message) { if (!message) return; // Html encode display name and message. - displayMessage(name, message); + var encodedName = name; + var encodedMsg = message.replace(/&/g, "&").replace(//g, ">"); + var messageEntry = createMessageEntry(encodedName, encodedMsg); + + var messageBox = document.getElementById('messages'); + messageBox.appendChild(messageEntry); + messageBox.scrollTop = messageBox.scrollHeight; }; // Create a function that the hub can call to broadcast messages. connection.on('broadcastMessage', messageCallback); @@ -144,38 +135,28 @@

Connect error: " + encodeMessage(errorMessage) + ". Reconnecting...

") - setTimeout(function () { - startConnectionWithRetry(connection) - }, getRandom(200, 2000)); - } - - function startConnectionWithRetry(hubConnection) { - hubConnection.start() - .then(function () { - $('#status').html("
Connected.
") - onConnected(hubConnection); - }) - .catch(onConnectionError); + var modal = document.getElementById('myModal'); + modal.classList.add('in'); + modal.style = 'display: block;'; } var connection = new signalR.HubConnectionBuilder() - .withUrl('/chat') - .build(); - + .withUrl('/chat') + .withHubProtocol(new signalR.protocols.msgpack.MessagePackHubProtocol()) + .build(); + bindConnectionMessage(connection); - startConnectionWithRetry(connection); + connection.start() + .then(function () { + onConnected(connection); + }) + .catch(function (error) { + console.error(error.message); + }); }); From 862fb42518662c106649cc7d9fa492e85a4351a1 Mon Sep 17 00:00:00 2001 From: "Liangying.Wei" Date: Fri, 21 Feb 2020 16:07:40 +0800 Subject: [PATCH 04/14] Fix settings (#831) --- .../ChatSample/ChatSample/Properties/launchSettings.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/samples/ChatSample/ChatSample/Properties/launchSettings.json b/samples/ChatSample/ChatSample/Properties/launchSettings.json index 70c36f99d..e2f7f8373 100644 --- a/samples/ChatSample/ChatSample/Properties/launchSettings.json +++ b/samples/ChatSample/ChatSample/Properties/launchSettings.json @@ -12,10 +12,11 @@ "commandName": "IISExpress", "launchBrowser": true, "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" + "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.Azure.SignalR" } }, - "ChatSample.Core3": { + "ChatSample": { "commandName": "Project", "launchBrowser": true, "applicationUrl": "http://localhost:5050", @@ -28,7 +29,8 @@ "commandName": "Project", "applicationUrl": "http://localhost:5050", "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" + "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.Azure.SignalR" } } } From 7929771034c4d9a64b35fb23e5da756f38cb94a8 Mon Sep 17 00:00:00 2001 From: JialinXin Date: Wed, 26 Feb 2020 14:43:07 +0800 Subject: [PATCH 05/14] [Part2.4][Live-scale] Support servers ping and update statusPing for all (#808) * support servers ping and update statusPing for all * resolve comments * minor update. * extract pingTimer to internal common use * resolve some comments * improve CustomizedPingTimer * minor fix * fix atomic operation. * fix thread-safe update * update UT as behavior changes * fix lock. --- .../ServiceConnectionManager.cs | 14 + .../Constants.cs | 7 +- .../Interfaces/IServiceConnectionContainer.cs | 9 + ...MultiEndpointServiceConnectionContainer.cs | 18 +- .../ServiceConnectionContainerBase.cs | 342 ++++++++++++++---- .../StrongServiceConnectionContainer.cs | 1 + .../WeakServiceConnectionContainer.cs | 101 +----- .../RuntimeServicePingMessages.cs | 5 + .../TestServiceConnectionContainer.cs | 14 + .../ServiceConnectionContainerBaseTests.cs | 160 +++++++- .../TestServiceConnectionContainer.cs | 5 +- 11 files changed, 512 insertions(+), 164 deletions(-) diff --git a/src/Microsoft.Azure.SignalR.AspNet/ServerConnections/ServiceConnectionManager.cs b/src/Microsoft.Azure.SignalR.AspNet/ServerConnections/ServiceConnectionManager.cs index efe54d1d8..36c400fa0 100644 --- a/src/Microsoft.Azure.SignalR.AspNet/ServerConnections/ServiceConnectionManager.cs +++ b/src/Microsoft.Azure.SignalR.AspNet/ServerConnections/ServiceConnectionManager.cs @@ -117,6 +117,20 @@ public virtual Task WriteAckableMessageAsync(ServiceMessage serviceMessage return _appConnection.WriteAckableMessageAsync(serviceMessage, cancellationToken); } + public Task StartGetServersPing() + { + return Task.WhenAll(GetConnections().Select(s => s.StartGetServersPing())); + } + + public Task StopGetServersPing() + { + return Task.WhenAll(GetConnections().Select(s => s.StopGetServersPing())); + } + + public HashSet GlobalServerIds => throw new NotSupportedException(); + + public bool HasClients => throw new NotSupportedException(); + private IEnumerable GetConnections() { if (_appConnection != null) diff --git a/src/Microsoft.Azure.SignalR.Common/Constants.cs b/src/Microsoft.Azure.SignalR.Common/Constants.cs index 70c7b0277..056a6d7be 100644 --- a/src/Microsoft.Azure.SignalR.Common/Constants.cs +++ b/src/Microsoft.Azure.SignalR.Common/Constants.cs @@ -12,7 +12,6 @@ internal static class Constants public const string ApplicationNameDefaultKey = "Azure:SignalR:ApplicationName"; public const int DefaultShutdownTimeoutInSeconds = 30; - public const int DefaultStatusPingIntervalInSeconds = 10; public const string AsrsMigrateFrom = "Asrs-Migrate-From"; public const string AsrsMigrateTo = "Asrs-Migrate-To"; @@ -63,5 +62,11 @@ public static class QueryParameter public const string ConnectionRequestId = "asrs_request_id"; public const string RequestCulture = "asrs_lang"; } + + public static class CustomizedPingTimer + { + public const string ServiceStatus = "ServiceStatus"; + public const string Servers = "Servers"; + } } } diff --git a/src/Microsoft.Azure.SignalR.Common/Interfaces/IServiceConnectionContainer.cs b/src/Microsoft.Azure.SignalR.Common/Interfaces/IServiceConnectionContainer.cs index 4ab2fabaf..584d0d07b 100644 --- a/src/Microsoft.Azure.SignalR.Common/Interfaces/IServiceConnectionContainer.cs +++ b/src/Microsoft.Azure.SignalR.Common/Interfaces/IServiceConnectionContainer.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.Azure.SignalR.Protocol; @@ -19,8 +20,16 @@ internal interface IServiceConnectionContainer Task WriteAckableMessageAsync(ServiceMessage serviceMessage, CancellationToken cancellationToken = default); + Task StartGetServersPing(); + + Task StopGetServersPing(); + ServiceConnectionStatus Status { get; } Task ConnectionInitializedTask { get; } + + HashSet GlobalServerIds { get; } + + bool HasClients { get; } } } \ No newline at end of file diff --git a/src/Microsoft.Azure.SignalR.Common/ServiceConnections/MultiEndpointServiceConnectionContainer.cs b/src/Microsoft.Azure.SignalR.Common/ServiceConnections/MultiEndpointServiceConnectionContainer.cs index 13e893806..739dc34dd 100644 --- a/src/Microsoft.Azure.SignalR.Common/ServiceConnections/MultiEndpointServiceConnectionContainer.cs +++ b/src/Microsoft.Azure.SignalR.Common/ServiceConnections/MultiEndpointServiceConnectionContainer.cs @@ -99,8 +99,12 @@ public Task ConnectionInitializedTask return Task.WhenAll(from connection in _connectionContainers select connection.Value.ConnectionInitializedTask); } - } - + } + + public HashSet GlobalServerIds => throw new NotSupportedException(); + + public bool HasClients => throw new NotSupportedException(); + public Task StartAsync() { return Task.WhenAll(_connectionContainers.Select(s => @@ -154,6 +158,16 @@ public async Task WriteAckableMessageAsync(ServiceMessage serviceMessage, return tcs.Task.IsCompleted; } + public Task StartGetServersPing() + { + return Task.WhenAll(ConnectionContainers.Select(c => c.Value.StartGetServersPing())); + } + + public Task StopGetServersPing() + { + return Task.WhenAll(ConnectionContainers.Select(c => c.Value.StopGetServersPing())); + } + internal IEnumerable GetRoutedEndpoints(ServiceMessage message) { if (!_routerEndpoints.needRouter) diff --git a/src/Microsoft.Azure.SignalR.Common/ServiceConnections/ServiceConnectionContainerBase.cs b/src/Microsoft.Azure.SignalR.Common/ServiceConnections/ServiceConnectionContainerBase.cs index e4be6300b..3bebdb308 100644 --- a/src/Microsoft.Azure.SignalR.Common/ServiceConnections/ServiceConnectionContainerBase.cs +++ b/src/Microsoft.Azure.SignalR.Common/ServiceConnections/ServiceConnectionContainerBase.cs @@ -1,8 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System; +using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -14,14 +15,23 @@ namespace Microsoft.Azure.SignalR { internal abstract class ServiceConnectionContainerBase : IServiceConnectionContainer, IServiceMessageHandler, IDisposable { - private static readonly int MaxReconnectBackOffInternalInMilliseconds = 1000; // Give interval(5s) * 24 = 2min window for retry considering abnormal case. private const int MaxRetryRemoveSeverConnection = 24; + + private static readonly int MaxReconnectBackOffInternalInMilliseconds = 1000; + private static readonly TimeSpan RemoveFromServiceTimeout = TimeSpan.FromSeconds(5); + private static readonly TimeSpan DefaultStatusPingInterval = TimeSpan.FromSeconds(10); + private static readonly TimeSpan DefaultServersPingInterval = TimeSpan.FromSeconds(5); + // Give (interval * 3 + 1) delay when check value expire. + private static readonly long DefaultServersPingTimeoutTicks = Stopwatch.Frequency * (DefaultServersPingInterval.Seconds * 3 + 1); + private static readonly Tuple, long> DefaultServerIdContext = new Tuple, long>(null, 0); + + private static readonly PingMessage _shutdownFinMessage = RuntimeServicePingMessage.GetFinPingMessage(false); + private static readonly PingMessage _shutdownFinMigratableMessage = RuntimeServicePingMessage.GetFinPingMessage(true); + private static TimeSpan ReconnectInterval => TimeSpan.FromMilliseconds(StaticRandom.Next(MaxReconnectBackOffInternalInMilliseconds)); - private static TimeSpan RemoveFromServiceTimeout = TimeSpan.FromSeconds(5); - private readonly BackOffPolicy _backOffPolicy = new BackOffPolicy(); private readonly object _lock = new object(); @@ -30,14 +40,17 @@ internal abstract class ServiceConnectionContainerBase : IServiceConnectionConta private readonly AckHandler _ackHandler; + private readonly CustomizedPingTimer _statusPing; + private readonly CustomizedPingTimer _serversPing; + private volatile List _fixedServiceConnections; private volatile ServiceConnectionStatus _status; - private volatile bool _terminated = false; - - private static readonly PingMessage _shutdownFinMessage = RuntimeServicePingMessage.GetFinPingMessage(false); - private static readonly PingMessage _shutdownFinMigratableMessage = RuntimeServicePingMessage.GetFinPingMessage(true); + // + private volatile Tuple, long> _serverIdContext; + private volatile bool _hasClients; + private volatile bool _terminated = false; protected ILogger Logger { get; } @@ -57,6 +70,10 @@ protected List FixedServiceConnections public event Action ConnectionStatusChanged; + public HashSet GlobalServerIds => _serverIdContext.Item1; + + public bool HasClients => _hasClients; + public ServiceConnectionStatus Status { get => _status; @@ -78,11 +95,11 @@ private set } } - protected ServiceConnectionContainerBase(IServiceConnectionFactory serviceConnectionFactory, - int minConnectionCount, - HubServiceEndpoint endpoint, - IReadOnlyList initialConnections = null, - ILogger logger = null, + protected ServiceConnectionContainerBase(IServiceConnectionFactory serviceConnectionFactory, + int minConnectionCount, + HubServiceEndpoint endpoint, + IReadOnlyList initialConnections = null, + ILogger logger = null, AckHandler ackHandler = null) { Logger = logger ?? throw new ArgumentNullException(nameof(logger)); @@ -117,6 +134,11 @@ protected ServiceConnectionContainerBase(IServiceConnectionFactory serviceConnec FixedServiceConnections = initial; FixedConnectionCount = initial.Count; ConnectionStatusChanged += OnStatusChanged; + + _statusPing = new CustomizedPingTimer(Logger, Constants.CustomizedPingTimer.ServiceStatus, WriteServiceStatusPingAsync, DefaultStatusPingInterval, DefaultStatusPingInterval); + _statusPing.Start(); + + _serversPing = new CustomizedPingTimer(Logger, Constants.CustomizedPingTimer.Servers, WriteServerIdsPingAsync, DefaultServersPingInterval, DefaultServersPingInterval); } public Task StartAsync() => Task.WhenAll(FixedServiceConnections.Select(c => StartCoreAsync(c))); @@ -133,9 +155,9 @@ public virtual Task StopAsync() /// protected async Task StartCoreAsync(IServiceConnection connection, string target = null) { - if (_terminated) - { - return; + if (_terminated) + { + return; } try @@ -148,7 +170,23 @@ protected async Task StartCoreAsync(IServiceConnection connection, string target } } - public abstract Task HandlePingAsync(PingMessage pingMessage); + public virtual Task HandlePingAsync(PingMessage pingMessage) + { + if (RuntimeServicePingMessage.TryGetStatus(pingMessage, out var status)) + { + Log.ReceivedServiceStatusPing(Logger, status, Endpoint); + _hasClients = status; + } + else if (RuntimeServicePingMessage.TryGetServerIds(pingMessage, out var serverIds, out var updatedTime)) + { + Log.ReceivedServerIdsPing(Logger, Endpoint); + if (updatedTime > _serverIdContext.Item2) + { + _serverIdContext = Tuple.Create(serverIds, updatedTime); + } + } + return Task.CompletedTask; + } public void HandleAck(AckMessage ackMessage) { @@ -250,8 +288,8 @@ protected void ReplaceFixedConnections(int index, IServiceConnection serviceConn } public Task ConnectionInitializedTask => Task.WhenAll(from connection in FixedServiceConnections - select connection.ConnectionInitializedTask); - + select connection.ConnectionInitializedTask); + public virtual Task WriteAsync(ServiceMessage serviceMessage) { return WriteToRandomAvailableConnection(serviceMessage); @@ -282,16 +320,34 @@ public async Task WriteAckableMessageAsync(ServiceMessage serviceMessage, // should not be hit. return false; } - } - - public virtual Task OfflineAsync(bool migratable) - { - return Task.WhenAll(FixedServiceConnections.Select(c => RemoveConnectionAsync(c, migratable))); + } + + public virtual Task OfflineAsync(bool migratable) + { + return Task.WhenAll(FixedServiceConnections.Select(c => RemoveConnectionAsync(c, migratable))); + } + + public Task StartGetServersPing() + { + if (_serversPing.Start()) + { + // reset old value when true start. + _serverIdContext = DefaultServerIdContext; + } + return Task.CompletedTask; + } + + public Task StopGetServersPing() + { + _serversPing.Stop(); + return Task.CompletedTask; } // Ready for scalable containers public void Dispose() { + _statusPing.Dispose(); + _serversPing.Dispose(); Dispose(true); GC.SuppressFinalize(this); } @@ -309,39 +365,39 @@ protected virtual ServiceConnectionStatus GetStatus() return FixedServiceConnections.Any(s => s.Status == ServiceConnectionStatus.Connected) ? ServiceConnectionStatus.Connected : ServiceConnectionStatus.Disconnected; - } - - protected async Task WriteFinAsync(IServiceConnection c, bool migratable) - { - if (migratable) - { - await c.WriteAsync(_shutdownFinMigratableMessage); - } - else - { - await c.WriteAsync(_shutdownFinMessage); - } - } - - protected async Task RemoveConnectionAsync(IServiceConnection c, bool migratable) - { - var retry = 0; - while (retry < MaxRetryRemoveSeverConnection) - { - using var source = new CancellationTokenSource(); - _ = WriteFinAsync(c, migratable); - - var task = await Task.WhenAny(c.ConnectionOfflineTask, Task.Delay(RemoveFromServiceTimeout, source.Token)); - - if (task == c.ConnectionOfflineTask) - { - source.Cancel(); - Log.ReceivedFinAckPing(Logger); - return; - } - retry++; - } - Log.TimeoutWaitingForFinAck(Logger, retry); + } + + protected async Task WriteFinAsync(IServiceConnection c, bool migratable) + { + if (migratable) + { + await c.WriteAsync(_shutdownFinMigratableMessage); + } + else + { + await c.WriteAsync(_shutdownFinMessage); + } + } + + protected async Task RemoveConnectionAsync(IServiceConnection c, bool migratable) + { + var retry = 0; + while (retry < MaxRetryRemoveSeverConnection) + { + using var source = new CancellationTokenSource(); + _ = WriteFinAsync(c, migratable); + + var task = await Task.WhenAny(c.ConnectionOfflineTask, Task.Delay(RemoveFromServiceTimeout, source.Token)); + + if (task == c.ConnectionOfflineTask) + { + source.Cancel(); + Log.ReceivedFinAckPing(Logger); + return; + } + retry++; + } + Log.TimeoutWaitingForFinAck(Logger, retry); } private Task WriteToRandomAvailableConnection(ServiceMessage serviceMessage) @@ -389,8 +445,120 @@ private IEnumerable CreateFixedServiceConnection(int count) { yield return CreateServiceConnectionCore(InitialConnectionType); } - } - + } + + private async Task WriteServiceStatusPingAsync() + { + await WriteAsync(RuntimeServicePingMessage.GetStatusPingMessage(true)); + } + + private async Task WriteServerIdsPingAsync() + { + if (Stopwatch.GetTimestamp() - _serverIdContext.Item2 > DefaultServersPingTimeoutTicks) + { + // reset value if expired. + _serverIdContext = DefaultServerIdContext; + } + await WriteAsync(RuntimeServicePingMessage.GetServersPingMessage()); + } + + private sealed class CustomizedPingTimer : IDisposable + { + private readonly object _lock = new object(); + private readonly long _defaultPingTicks; + + private readonly string _pingName; + private readonly Func _writePing; + private readonly TimeSpan _dueTime; + private readonly TimeSpan _intervalTime; + private readonly ILogger _logger; + + // Considering parallel add endpoints to save time, + // Add a counter control multiple time call Start() and Stop() correctly. + private long _counter = 0; + + private long _lastSendTimestamp = 0; + private TimerAwaitable _timer; + + public CustomizedPingTimer(ILogger logger, string pingName, Func writePing, TimeSpan dueTime, TimeSpan intervalTime) + { + _logger = logger; + _pingName = pingName; + _writePing = writePing; + _dueTime = dueTime; + _intervalTime = intervalTime; + _defaultPingTicks = intervalTime.Seconds * Stopwatch.Frequency; + + _timer = Init(); + } + + public bool Start() + { + if (Interlocked.Increment(ref _counter) == 1) + { + _timer.Start(); + _ = PingAsync(_timer); + return true; + } + return false; + } + + public void Stop() + { + // might be called by multi-thread, lock to ensure thread-safe for _counter update + lock (_lock) + { + if (Interlocked.Read(ref _counter) == 0) + { + // Avoid wrong Stop() to break _counter in further scale + Log.TimerAlreadyStopped(_logger, _pingName); + return; + } + if (Interlocked.Decrement(ref _counter) == 0) + { + _timer.Stop(); + } + } + } + + public void Dispose() + { + _timer.Stop(); + } + + private TimerAwaitable Init() + { + Log.StartingPingTimer(_logger, _pingName, _intervalTime); + + _lastSendTimestamp = Stopwatch.GetTimestamp(); + var timer = new TimerAwaitable(_dueTime, _intervalTime); + + return timer; + } + + private async Task PingAsync(TimerAwaitable timer) + { + while (await timer) + { + try + { + // Check if last send time is longer than default keep-alive ticks and then send ping + if (Stopwatch.GetTimestamp() - Interlocked.Read(ref _lastSendTimestamp) > _defaultPingTicks) + { + await _writePing.Invoke(); + + Interlocked.Exchange(ref _lastSendTimestamp, Stopwatch.GetTimestamp()); + Log.SentPing(_logger, _pingName); + } + } + catch (Exception e) + { + Log.FailedSendingPing(_logger, _pingName, e); + } + } + } + } + private static class Log { private static readonly Action _endpointOnline = @@ -398,12 +566,30 @@ private static class Log private static readonly Action _endpointOffline = LoggerMessage.Define(LogLevel.Error, new EventId(2, "EndpointOffline"), "Hub '{hub}' is now disconnected from '{endpoint}'. Please check log for detailed info."); - + private static readonly Action _receivedFinAckPing = - LoggerMessage.Define(LogLevel.Information, new EventId(3, "ReceivedFinAckPing"), "Received FinAck ping."); - + LoggerMessage.Define(LogLevel.Information, new EventId(3, "ReceivedFinAckPing"), "Received FinAck ping."); + private static readonly Action _timeoutWaitingForFinAck = - LoggerMessage.Define(LogLevel.Error, new EventId(4, "TimeoutWaitingForFinAck"), "Fail to receive FinAckPing after retry {retryCount} times."); + LoggerMessage.Define(LogLevel.Error, new EventId(4, "TimeoutWaitingForFinAck"), "Fail to receive FinAckPing after retry {retryCount} times."); + + private static readonly Action _startingPingTimer = + LoggerMessage.Define(LogLevel.Debug, new EventId(5, "StartingPingTimer"), "Starting { pingName } ping timer. Duration: {KeepAliveInterval:0.00}ms"); + + private static readonly Action _sentPing = + LoggerMessage.Define(LogLevel.Debug, new EventId(6, "SentPing"), "Sent a { pingName } ping message to service."); + + private static readonly Action _failedSendingPing = + LoggerMessage.Define(LogLevel.Warning, new EventId(7, "FailedSendingPing"), "Failed sending a { pingName } ping message to service."); + + private static readonly Action _receivedServiceStatusPing = + LoggerMessage.Define(LogLevel.Debug, new EventId(8, "ReceivedServiceStatusPing"), "Received a service status active={isActive} from {endpoint} for hub {hub}."); + + private static readonly Action _receivedServerIdsPing = + LoggerMessage.Define(LogLevel.Debug, new EventId(9, "ReceivedServerIdsPing"), "Received a server ids ping from {endpoint} for hub {hub}."); + + private static readonly Action _timerAlreadyStopped = + LoggerMessage.Define(LogLevel.Warning, new EventId(10, "TimerAlreadyStopped"), "Failed to stop {pingName} timer as it's not started"); public static void EndpointOnline(ILogger logger, HubServiceEndpoint endpoint) { @@ -424,6 +610,36 @@ public static void TimeoutWaitingForFinAck(ILogger logger, int retryCount) { _timeoutWaitingForFinAck(logger, retryCount, null); } + + public static void StartingPingTimer(ILogger logger, string pingName, TimeSpan keepAliveInterval) + { + _startingPingTimer(logger, pingName, keepAliveInterval.TotalMilliseconds, null); + } + + public static void SentPing(ILogger logger, string pingName) + { + _sentPing(logger, pingName, null); + } + + public static void FailedSendingPing(ILogger logger, string pingName, Exception exception) + { + _failedSendingPing(logger, pingName, exception); + } + + public static void ReceivedServiceStatusPing(ILogger logger, bool isActive, HubServiceEndpoint endpoint) + { + _receivedServiceStatusPing(logger, isActive, endpoint, endpoint.Hub, null); + } + + public static void ReceivedServerIdsPing(ILogger logger, HubServiceEndpoint endpoint) + { + _receivedServerIdsPing(logger, endpoint, endpoint.Hub, null); + } + + public static void TimerAlreadyStopped(ILogger logger, string pingName) + { + _timerAlreadyStopped(logger, pingName, null); + } } } } diff --git a/src/Microsoft.Azure.SignalR.Common/ServiceConnections/StrongServiceConnectionContainer.cs b/src/Microsoft.Azure.SignalR.Common/ServiceConnections/StrongServiceConnectionContainer.cs index 2e9c248fa..56c59e1f3 100644 --- a/src/Microsoft.Azure.SignalR.Common/ServiceConnections/StrongServiceConnectionContainer.cs +++ b/src/Microsoft.Azure.SignalR.Common/ServiceConnections/StrongServiceConnectionContainer.cs @@ -28,6 +28,7 @@ public StrongServiceConnectionContainer( public override Task HandlePingAsync(PingMessage pingMessage) { + base.HandlePingAsync(pingMessage); if (RuntimeServicePingMessage.TryGetRebalance(pingMessage, out var target) && !string.IsNullOrEmpty(target)) { var connection = CreateOnDemandServiceConnection(); diff --git a/src/Microsoft.Azure.SignalR.Common/ServiceConnections/WeakServiceConnectionContainer.cs b/src/Microsoft.Azure.SignalR.Common/ServiceConnections/WeakServiceConnectionContainer.cs index 7b6b8413a..c5a15f390 100644 --- a/src/Microsoft.Azure.SignalR.Common/ServiceConnections/WeakServiceConnectionContainer.cs +++ b/src/Microsoft.Azure.SignalR.Common/ServiceConnections/WeakServiceConnectionContainer.cs @@ -13,36 +13,28 @@ namespace Microsoft.Azure.SignalR.Common.ServiceConnections internal class WeakServiceConnectionContainer : ServiceConnectionContainerBase { private const int CheckWindow = 5; - private static readonly TimeSpan DefaultGetServiceStatusInterval = TimeSpan.FromSeconds(Constants.DefaultStatusPingIntervalInSeconds); - private static readonly long DefaultGetServiceStatusTicks = DefaultGetServiceStatusInterval.Seconds * Stopwatch.Frequency; private static readonly TimeSpan CheckTimeSpan = TimeSpan.FromMinutes(10); private readonly object _lock = new object(); private int _inactiveCount; private DateTime? _firstInactiveTime; - private long _lastSendTimestamp; // active ones are those whose client connections connected to the whole endpoint private volatile bool _active = true; - private readonly TimerAwaitable _timer; - protected override ServiceConnectionType InitialConnectionType => ServiceConnectionType.Weak; public WeakServiceConnectionContainer(IServiceConnectionFactory serviceConnectionFactory, int fixedConnectionCount, HubServiceEndpoint endpoint, ILogger logger) : base(serviceConnectionFactory, fixedConnectionCount, endpoint, logger: logger) { - _timer = StartServiceStatusPingTimer(); } public override Task HandlePingAsync(PingMessage pingMessage) { - if (RuntimeServicePingMessage.TryGetStatus(pingMessage, out var status)) - { - _active = GetServiceStatus(status, CheckWindow, CheckTimeSpan); - Log.ReceivedServiceStatusPing(Logger, status, Endpoint); - } + base.HandlePingAsync(pingMessage); + var active = HasClients; + _active = GetServiceStatus(active, CheckWindow, CheckTimeSpan); return Task.CompletedTask; } @@ -59,9 +51,9 @@ public override Task WriteAsync(ServiceMessage serviceMessage) return base.WriteAsync(serviceMessage); } - public override Task OfflineAsync(bool migratable) - { - return Task.CompletedTask; + public override Task OfflineAsync(bool migratable) + { + return Task.CompletedTask; } internal bool GetServiceStatus(bool active, int checkWindow, TimeSpan checkTimeSpan) @@ -96,94 +88,15 @@ internal bool GetServiceStatus(bool active, int checkWindow, TimeSpan checkTimeS } } - private TimerAwaitable StartServiceStatusPingTimer() - { - Log.StartingServiceStatusPingTimer(Logger, DefaultGetServiceStatusInterval); - - _lastSendTimestamp = Stopwatch.GetTimestamp(); - var timer = new TimerAwaitable(DefaultGetServiceStatusInterval, DefaultGetServiceStatusInterval); - _ = ServiceStatusPingAsync(timer); - - return timer; - } - - private async Task ServiceStatusPingAsync(TimerAwaitable timer) - { - using (timer) - { - timer.Start(); - - while (await timer) - { - try - { - // Check if last send time is longer than default keep-alive ticks and then send ping - if (Stopwatch.GetTimestamp() - Interlocked.Read(ref _lastSendTimestamp) > DefaultGetServiceStatusTicks) - { - await WriteAsync(RuntimeServicePingMessage.GetStatusPingMessage(true)); - Interlocked.Exchange(ref _lastSendTimestamp, Stopwatch.GetTimestamp()); - Log.SentServiceStatusPing(Logger); - } - } - catch (Exception e) - { - Log.FailedSendingServiceStatusPing(Logger, e); - } - } - } - } - - protected override void Dispose(bool disposing) - { - if (disposing) - { - _timer.Stop(); - } - - base.Dispose(disposing); - } - private static class Log { - private static readonly Action _startingServiceStatusPingTimer = - LoggerMessage.Define(LogLevel.Debug, new EventId(0, "StartingServiceStatusPingTimer"), "Starting service status ping timer. Duration: {KeepAliveInterval:0.00}ms"); - - private static readonly Action _sentServiceStatusPing = - LoggerMessage.Define(LogLevel.Debug, new EventId(1, "SentServiceStatusPing"), "Sent a service status ping message to service."); - - private static readonly Action _failedSendingServiceStatusPing = - LoggerMessage.Define(LogLevel.Warning, new EventId(2, "FailedSendingServiceStatusPing"), "Failed sending a service status ping message to service."); - private static readonly Action _ignoreSendingMessageToInactiveEndpoint = - LoggerMessage.Define(LogLevel.Debug, new EventId(3, "IgnoreSendingMessageToInactiveEndpoint"), "Message {type} sending to {endpoint} for hub {hub} is ignored because the endpoint is inactive."); - - private static readonly Action _receivedServiceStatusPing = - LoggerMessage.Define(LogLevel.Debug, new EventId(4, "ReceivedServiceStatusPing"), "Received a service status active={isActive} from {endpoint} for hub {hub}."); - - public static void StartingServiceStatusPingTimer(ILogger logger, TimeSpan keepAliveInterval) - { - _startingServiceStatusPingTimer(logger, keepAliveInterval.TotalMilliseconds, null); - } - - public static void SentServiceStatusPing(ILogger logger) - { - _sentServiceStatusPing(logger, null); - } - - public static void FailedSendingServiceStatusPing(ILogger logger, Exception exception) - { - _failedSendingServiceStatusPing(logger, exception); - } + LoggerMessage.Define(LogLevel.Debug, new EventId(1, "IgnoreSendingMessageToInactiveEndpoint"), "Message {type} sending to {endpoint} for hub {hub} is ignored because the endpoint is inactive."); public static void IgnoreSendingMessageToInactiveEndpoint(ILogger logger, Type messageType, HubServiceEndpoint endpoint) { _ignoreSendingMessageToInactiveEndpoint(logger, messageType.Name, endpoint, endpoint.Hub, null); } - - public static void ReceivedServiceStatusPing(ILogger logger, bool isActive, HubServiceEndpoint endpoint) - { - _receivedServiceStatusPing(logger, isActive, endpoint, endpoint.Hub, null); - } } } } diff --git a/src/Microsoft.Azure.SignalR.Common/ServiceMessages/RuntimeServicePingMessages.cs b/src/Microsoft.Azure.SignalR.Common/ServiceMessages/RuntimeServicePingMessages.cs index 0e6bf82e3..a5bdce838 100644 --- a/src/Microsoft.Azure.SignalR.Common/ServiceMessages/RuntimeServicePingMessages.cs +++ b/src/Microsoft.Azure.SignalR.Common/ServiceMessages/RuntimeServicePingMessages.cs @@ -84,12 +84,17 @@ public static ServicePingMessage GetFinPingMessage(bool migratable) => public static ServicePingMessage GetServersPingMessage() => GetServerIds; + // for test public static bool IsFin(this ServiceMessage serviceMessage) => serviceMessage is ServicePingMessage ping && TryGetValue(ping, ShutdownKey, out var value) && (value == ShutdownFinValue || value == ShutdownFinMigratableValue); public static bool IsFinAck(this ServicePingMessage ping) => TryGetValue(ping, ShutdownKey, out var value) && value == ShutdownFinAckValue; + // for test + public static bool IsGetServers(this ServiceMessage serviceMessage) => + serviceMessage is ServicePingMessage ping && TryGetValue(ping, ServersKey, out _); + internal static bool TryGetValue(ServicePingMessage pingMessage, string key, out string value) { if (pingMessage == null) diff --git a/test/Microsoft.Azure.SignalR.Tests.Common/TestClasses/TestServiceConnectionContainer.cs b/test/Microsoft.Azure.SignalR.Tests.Common/TestClasses/TestServiceConnectionContainer.cs index 469ebf0fe..2e3bf1ae3 100644 --- a/test/Microsoft.Azure.SignalR.Tests.Common/TestClasses/TestServiceConnectionContainer.cs +++ b/test/Microsoft.Azure.SignalR.Tests.Common/TestClasses/TestServiceConnectionContainer.cs @@ -25,6 +25,10 @@ internal sealed class TestServiceConnectionContainer : IServiceConnectionContain public IReadOnlyDictionary ConnectionContainers { get; } + public HashSet GlobalServerIds => throw new NotSupportedException(); + + public bool HasClients => throw new NotSupportedException(); + public TestServiceConnectionContainer(ServiceConnectionStatus status) { Status = status; @@ -69,5 +73,15 @@ public Task OfflineAsync(bool migratable) { return Task.CompletedTask; } + + public Task StartGetServersPing() + { + return Task.CompletedTask; + } + + public Task StopGetServersPing() + { + return Task.CompletedTask; + } } } \ No newline at end of file diff --git a/test/Microsoft.Azure.SignalR.Tests/ServiceConnectionContainerBaseTests.cs b/test/Microsoft.Azure.SignalR.Tests/ServiceConnectionContainerBaseTests.cs index f113035bb..fb3424fc4 100644 --- a/test/Microsoft.Azure.SignalR.Tests/ServiceConnectionContainerBaseTests.cs +++ b/test/Microsoft.Azure.SignalR.Tests/ServiceConnectionContainerBaseTests.cs @@ -1,14 +1,22 @@ using System; using System.Collections.Generic; -using System.Threading; +using System.Linq; using System.Threading.Tasks; + using Microsoft.Azure.SignalR.Protocol; +using Microsoft.Azure.SignalR.Tests.Common; +using Microsoft.Extensions.Logging; using Xunit; +using Xunit.Abstractions; namespace Microsoft.Azure.SignalR.Tests { - public class ServiceConnectionContainerBaseTests + public class ServiceConnectionContainerBaseTests : VerifiableLoggedTest { + public ServiceConnectionContainerBaseTests(ITestOutputHelper helper) : base(helper) + { } + + [Fact] public async Task TestIfConnectionWillRestartAfterShutdown() { @@ -55,6 +63,147 @@ public async Task TestOffline(bool migratable) } } + [Theory] + [InlineData(3, 3, 0)] + [InlineData(0, 1, 1)] // stop more than start will log warn + [InlineData(1, 2, 1)] // stop more than start will log warn + [InlineData(3, 1, 0)] + public async Task TestServerIdsPing(int startCount, int stopCount, int expectedWarn) + { + using (StartVerifiableLog(out var loggerFactory, LogLevel.Warning, logChecker: logs => + { + var warns = logs.Where(s => s.Write.LogLevel == LogLevel.Warning).ToList(); + Assert.Equal(expectedWarn, warns.Count); + if (expectedWarn > 0) + { + Assert.Contains(warns, s => s.Write.Message.Contains("Failed to stop Servers timer as it's not started")); + } + return true; + })) + { + List connections = new List + { + new SimpleTestServiceConnection(), + new SimpleTestServiceConnection(), + new SimpleTestServiceConnection() + }; + using TestServiceConnectionContainer container = + new TestServiceConnectionContainer( + connections, + factory: new SimpleTestServiceConnectionFactory(), + logger: loggerFactory.CreateLogger()); + + await container.StartAsync(); + + var tasks = new List(); + + while (startCount > 0) + { + tasks.Add(container.StartGetServersPing()); + startCount--; + } + await Task.WhenAll(tasks); + + // default interval is 5s, add 2s for delay, validate any one connection write servers ping. + if (tasks.Count > 0) + { + await Task.WhenAny(connections.Select(c => + { + var connection = c as SimpleTestServiceConnection; + return connection.ServerIdsPingTask.OrTimeout(7000); + })); + } + + tasks.Clear(); + while (stopCount > 0) + { + tasks.Add(container.StopGetServersPing()); + stopCount--; + } + await Task.WhenAll(tasks); + } + } + + [Theory] + [InlineData(1, 1, 3, 3, 0)] + [InlineData(1, 1, 0, 1, 1)] + [InlineData(1, 1, 1, 0, 0)] + [InlineData(1, 3, 2, 2, 2)] // first time error stop won't break second time write. + public async Task TestServerIdsPingWorkSecondTime(int firstStart, int firstStop, int secondStart, int secondStop, int expectedWarn) + { + using (StartVerifiableLog(out var loggerFactory, LogLevel.Warning, logChecker: logs => + { + var warns = logs.Where(s => s.Write.LogLevel == LogLevel.Warning).ToList(); + Assert.Equal(expectedWarn, warns.Count); + if (expectedWarn > 0) + { + Assert.Contains(warns, s => s.Write.Message.Contains("Failed to stop Servers timer as it's not started")); + } + return true; + })) + { + List connections = new List + { + new SimpleTestServiceConnection(), + new SimpleTestServiceConnection(), + new SimpleTestServiceConnection() + }; + using TestServiceConnectionContainer container = + new TestServiceConnectionContainer( + connections, + factory: new SimpleTestServiceConnectionFactory(), + logger: loggerFactory.CreateLogger()); + + await container.StartAsync(); + + var tasks = new List(); + + // first time scale + while (firstStart > 0) + { + tasks.Add(container.StartGetServersPing()); + firstStart--; + } + await Task.WhenAll(tasks); + + tasks.Clear(); + while (firstStop > 0) + { + tasks.Add(container.StopGetServersPing()); + firstStop--; + } + await Task.WhenAll(tasks); + + // second time scale + tasks.Clear(); + while (secondStart > 0) + { + tasks.Add(container.StartGetServersPing()); + secondStart--; + } + await Task.WhenAll(tasks); + + // default interval is 5s, add 2s for delay, validate any one connection write servers ping. + if (tasks.Count > 0) + { + await Task.WhenAny(connections.Select(c => + { + var connection = c as SimpleTestServiceConnection; + return connection.ServerIdsPingTask.OrTimeout(7000); + })); + } + + tasks.Clear(); + while (secondStop > 0) + { + tasks.Add(container.StopGetServersPing()); + secondStop--; + } + await Task.WhenAll(tasks); + } + } + + private sealed class SimpleTestServiceConnectionFactory : IServiceConnectionFactory { public IServiceConnection Create(HubServiceEndpoint endpoint, IServiceMessageHandler serviceMessageHandler, ServiceConnectionType type) => new SimpleTestServiceConnection(); @@ -67,9 +216,12 @@ private sealed class SimpleTestServiceConnection : IServiceConnection public ServiceConnectionStatus Status { get; set; } = ServiceConnectionStatus.Disconnected; private readonly TaskCompletionSource _offline = new TaskCompletionSource(TaskContinuationOptions.RunContinuationsAsynchronously); + private readonly TaskCompletionSource _serverIdsPing = new TaskCompletionSource(TaskContinuationOptions.RunContinuationsAsynchronously); public Task ConnectionOfflineTask => _offline.Task; + public Task ServerIdsPingTask => _serverIdsPing.Task; + public SimpleTestServiceConnection(ServiceConnectionStatus status = ServiceConnectionStatus.Disconnected) { Status = status; @@ -98,6 +250,10 @@ public Task WriteAsync(ServiceMessage serviceMessage) { _offline.SetResult(true); } + if (RuntimeServicePingMessage.IsGetServers(serviceMessage)) + { + _serverIdsPing.SetResult(true); + } return Task.CompletedTask; } } diff --git a/test/Microsoft.Azure.SignalR.Tests/TestServiceConnectionContainer.cs b/test/Microsoft.Azure.SignalR.Tests/TestServiceConnectionContainer.cs index 9d865bf33..1ee5b4e0f 100644 --- a/test/Microsoft.Azure.SignalR.Tests/TestServiceConnectionContainer.cs +++ b/test/Microsoft.Azure.SignalR.Tests/TestServiceConnectionContainer.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Azure.SignalR.Protocol; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; namespace Microsoft.Azure.SignalR.Tests @@ -16,8 +17,8 @@ internal sealed class TestServiceConnectionContainer : ServiceConnectionContaine public bool MockOffline { get; set; } = false; - public TestServiceConnectionContainer(List serviceConnections, HubServiceEndpoint endpoint = null, AckHandler ackHandler = null, IServiceConnectionFactory factory = null) - : base(factory, 0, endpoint, serviceConnections, ackHandler: ackHandler, logger: NullLogger.Instance) + public TestServiceConnectionContainer(List serviceConnections, HubServiceEndpoint endpoint = null, AckHandler ackHandler = null, IServiceConnectionFactory factory = null, ILogger logger = null) + : base(factory, 0, endpoint, serviceConnections, ackHandler: ackHandler, logger: logger ?? NullLogger.Instance) { } From 5f1f13fe80bbb494bfcd8f497374189c1a6eac6a Mon Sep 17 00:00:00 2001 From: JialinXin Date: Mon, 2 Mar 2020 13:02:32 +0800 Subject: [PATCH 06/14] [Part3.2][Live-scale] add interfaces for scale ServiceEndpoint (#832) * add interfaces for Scale ServiceEndpoint * add timeout check in ServiceEndpointManager when scale * improve timeout * better naming. --- .../Constants.cs | 1 + .../Endpoints/HubServiceEndpoint.cs | 22 ++- .../Endpoints/ServiceEndpointManagerBase.cs | 184 +++++++++++++++++- .../Interfaces/IServiceEndpointManager.cs | 8 + ...MultiEndpointServiceConnectionContainer.cs | 57 +++++- .../ServiceEndpointManager.cs | 62 +++++- src/Microsoft.Azure.SignalR/ServiceOptions.cs | 8 +- 7 files changed, 331 insertions(+), 11 deletions(-) diff --git a/src/Microsoft.Azure.SignalR.Common/Constants.cs b/src/Microsoft.Azure.SignalR.Common/Constants.cs index 056a6d7be..863f3dbbd 100644 --- a/src/Microsoft.Azure.SignalR.Common/Constants.cs +++ b/src/Microsoft.Azure.SignalR.Common/Constants.cs @@ -12,6 +12,7 @@ internal static class Constants public const string ApplicationNameDefaultKey = "Azure:SignalR:ApplicationName"; public const int DefaultShutdownTimeoutInSeconds = 30; + public const int DefaultScaleTimeoutInSeconds = 300; public const string AsrsMigrateFrom = "Asrs-Migrate-From"; public const string AsrsMigrateTo = "Asrs-Migrate-To"; diff --git a/src/Microsoft.Azure.SignalR.Common/Endpoints/HubServiceEndpoint.cs b/src/Microsoft.Azure.SignalR.Common/Endpoints/HubServiceEndpoint.cs index 2eb65a537..8df4bede2 100644 --- a/src/Microsoft.Azure.SignalR.Common/Endpoints/HubServiceEndpoint.cs +++ b/src/Microsoft.Azure.SignalR.Common/Endpoints/HubServiceEndpoint.cs @@ -1,14 +1,24 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Threading.Tasks; + namespace Microsoft.Azure.SignalR { internal class HubServiceEndpoint : ServiceEndpoint { - public HubServiceEndpoint(string hub, IServiceEndpointProvider provider, ServiceEndpoint endpoint) : base(endpoint) + private readonly TaskCompletionSource _scaleTcs; + + public HubServiceEndpoint( + string hub, + IServiceEndpointProvider provider, + ServiceEndpoint endpoint, + bool needScaleTcs = false + ) : base(endpoint) { Hub = hub; Provider = provider; + _scaleTcs = needScaleTcs ? new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously) : null; } internal HubServiceEndpoint() : base() { } @@ -16,5 +26,15 @@ internal HubServiceEndpoint() : base() { } public string Hub { get; } public IServiceEndpointProvider Provider { get; } + + /// + /// Task waiting for HubServiceEndpoint turn ready when live add/remove endpoint + /// + public Task ScaleTask => _scaleTcs?.Task ?? Task.CompletedTask; + + public void CompleteScale() + { + _scaleTcs?.TrySetResult(true); + } } } diff --git a/src/Microsoft.Azure.SignalR.Common/Endpoints/ServiceEndpointManagerBase.cs b/src/Microsoft.Azure.SignalR.Common/Endpoints/ServiceEndpointManagerBase.cs index 5b95328b9..06ce0208a 100644 --- a/src/Microsoft.Azure.SignalR.Common/Endpoints/ServiceEndpointManagerBase.cs +++ b/src/Microsoft.Azure.SignalR.Common/Endpoints/ServiceEndpointManagerBase.cs @@ -5,6 +5,9 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Threading; +using System.Threading.Tasks; + using Microsoft.Azure.SignalR.Common; using Microsoft.Extensions.Logging; @@ -20,6 +23,10 @@ internal abstract class ServiceEndpointManagerBase : IServiceEndpointManager // Filtered valuable endpoints from ServiceOptions public ServiceEndpoint[] Endpoints { get; protected set; } + public event EndpointEventHandler OnAdd; + public event EndpointEventHandler OnRemove; + public event EndpointEventHandler OnRename; + protected ServiceEndpointManagerBase(IServiceEndpointOptions options, ILogger logger) : this(GetEndpoints(options), logger) { @@ -43,11 +50,7 @@ internal ServiceEndpointManagerBase(IEnumerable endpoints, ILog public IReadOnlyList GetEndpoints(string hub) { - return _endpointsPerHub.GetOrAdd(hub, s => Endpoints.Select(e => - { - var provider = GetEndpointProvider(e); - return new HubServiceEndpoint(hub, provider, e); - }).ToArray()); + return _endpointsPerHub.GetOrAdd(hub, s => Endpoints.Select(e => CreateHubServiceEndpoint(hub, e)).ToArray()); } protected static IEnumerable GetEndpoints(IServiceEndpointOptions options) @@ -96,15 +99,186 @@ protected ServiceEndpoint[] GetValuableEndpoints(IEnumerable en return groupedEndpoints.ToArray(); } + protected async Task AddServiceEndpointsAsync(IReadOnlyList endpoints, CancellationToken cancellationToken) + { + if (endpoints.Count > 0) + { + try + { + var hubEndpoints = CreateHubServiceEndpoints(endpoints, true); + + await Task.WhenAll(hubEndpoints.Select(e => AddHubServiceEndpointAsync(e, cancellationToken))); + + // TODO: update local store for negotiation + } + catch (Exception ex) + { + Log.FailedAddingEndpoints(_logger, ex); + } + } + } + + protected async Task RemoveServiceEndpointsAsync(IReadOnlyList endpoints, CancellationToken cancellationToken) + { + if (endpoints.Count > 0) + { + try + { + var hubEndpoints = CreateHubServiceEndpoints(endpoints, true); + + // TODO: update local store for negotiation + + await Task.WhenAll(hubEndpoints.Select(e => RemoveHubServiceEndpointAsync(e, cancellationToken))); + } + catch (Exception ex) + { + Log.FailedRemovingEndpoints(_logger, ex); + } + } + } + + protected Task RenameSerivceEndpoints(IReadOnlyList endpoints) + { + if (endpoints.Count > 0) + { + try + { + var hubEndpoints = CreateHubServiceEndpoints(endpoints, false); + + // TODO: update local store for negotiation + + return Task.WhenAll(hubEndpoints.Select(e => RenameHubServiceEndpoint(e))); + } + catch (Exception ex) + { + Log.FailedRenamingEndpoint(_logger, ex); + } + } + return Task.CompletedTask; + } + + private HubServiceEndpoint CreateHubServiceEndpoint(string hub, ServiceEndpoint endpoint, bool needScaleTcs = false) + { + var provider = GetEndpointProvider(endpoint); + + return new HubServiceEndpoint(hub, provider, endpoint, needScaleTcs); + } + + private IReadOnlyList CreateHubServiceEndpoints(string hub, IEnumerable endpoints, bool needScaleTcs) + { + return endpoints.Select(e => CreateHubServiceEndpoint(hub, e, needScaleTcs)).ToList(); + } + + private IReadOnlyList CreateHubServiceEndpoints(IEnumerable endpoints, bool needScaleTcs) + { + var hubEndpoints = new List(); + var hubs = _endpointsPerHub.Keys; + foreach (var hub in hubs) + { + hubEndpoints.AddRange(CreateHubServiceEndpoints(hub, endpoints, needScaleTcs)); + } + return hubEndpoints; + } + + private async Task AddHubServiceEndpointAsync(HubServiceEndpoint endpoint, CancellationToken cancellationToken) + { + Log.StartAddingEndpoint(_logger, endpoint.Endpoint, endpoint.Name); + + OnAdd?.Invoke(endpoint); + + // Wait for new endpoint turn Ready or timeout getting cancelled + await Task.WhenAny(endpoint.ScaleTask, cancellationToken.AsTask()); + + // Set complete + endpoint.CompleteScale(); + } + + private async Task RemoveHubServiceEndpointAsync(HubServiceEndpoint endpoint, CancellationToken cancellationToken) + { + Log.StartRemovingEndpoint(_logger, endpoint.Endpoint, endpoint.Name); + + OnRemove?.Invoke(endpoint); + + // Wait for endpoint turn offline or timeout getting cancelled + await Task.WhenAny(endpoint.ScaleTask, cancellationToken.AsTask()); + + // Set complete + endpoint.CompleteScale(); + } + + private Task RenameHubServiceEndpoint(HubServiceEndpoint endpoint) + { + Log.StartRenamingEndpoint(_logger, endpoint.Endpoint, endpoint.Name); + + OnRename?.Invoke(endpoint); + return Task.CompletedTask; + } + private static class Log { private static readonly Action _duplicateEndpointFound = LoggerMessage.Define(LogLevel.Warning, new EventId(1, "DuplicateEndpointFound"), "{count} endpoint configurations to '{endpoint}' found, use '{name}'."); + private static readonly Action _startAddingEndpoint = + LoggerMessage.Define(LogLevel.Debug, new EventId(2, "StartAddingEndpoint"), "Start adding endpoint: '{endpoint}', name: '{name}'."); + + private static readonly Action _startRemovingEndpoint = + LoggerMessage.Define(LogLevel.Debug, new EventId(3, "StartRemovingEndpoint"), "Start removing endpoint: '{endpoint}', name: '{name}'"); + + private static readonly Action _startRenamingEndpoint = + LoggerMessage.Define(LogLevel.Debug, new EventId(4, "StartRenamingEndpoint"), "Start renaming endpoint: '{endpoint}', name: '{name}'"); + + private static readonly Action _failedAddingEndpoints = + LoggerMessage.Define(LogLevel.Error, new EventId(5, "FailedAddingEndpoints"), "Failed adding endpoints."); + + private static readonly Action _failedRemovingEndpoints = + LoggerMessage.Define(LogLevel.Error, new EventId(6, "FailedRemovingEndpoints"), "Failed removing endpoints."); + + private static readonly Action _failedRenamingEndpoints = + LoggerMessage.Define(LogLevel.Error, new EventId(7, "StartRenamingEndpoints"), "Failed renaming endpoints."); + + private static readonly Action _timeoutWaitingForScale = + LoggerMessage.Define(LogLevel.Error, new EventId(8, "TimeoutWaitingForScale"), "Timeout waiting '{timeout}' seconds for connection operations when scale endpoint."); + public static void DuplicateEndpointFound(ILogger logger, int count, string endpoint, string name) { _duplicateEndpointFound(logger, count, endpoint, name, null); } + + public static void StartAddingEndpoint(ILogger logger, string endpoint, string name) + { + _startAddingEndpoint(logger, endpoint, name, null); + } + + public static void StartRemovingEndpoint(ILogger logger, string endpoint, string name) + { + _startRemovingEndpoint(logger, endpoint, name, null); + } + + public static void StartRenamingEndpoint(ILogger logger, string endpoint, string name) + { + _startRenamingEndpoint(logger, endpoint, name, null); + } + + public static void FailedAddingEndpoints(ILogger logger, Exception ex) + { + _failedAddingEndpoints(logger, ex); + } + + public static void FailedRemovingEndpoints(ILogger logger, Exception ex) + { + _failedRemovingEndpoints(logger, ex); + } + + public static void FailedRenamingEndpoint(ILogger logger, Exception ex) + { + _failedRenamingEndpoints(logger, ex); + } + + public static void TimeoutWaitingForScale(ILogger logger, int timeout) + { + _timeoutWaitingForScale(logger, timeout, null); + } } } } diff --git a/src/Microsoft.Azure.SignalR.Common/Interfaces/IServiceEndpointManager.cs b/src/Microsoft.Azure.SignalR.Common/Interfaces/IServiceEndpointManager.cs index 7349bb812..1197b4772 100644 --- a/src/Microsoft.Azure.SignalR.Common/Interfaces/IServiceEndpointManager.cs +++ b/src/Microsoft.Azure.SignalR.Common/Interfaces/IServiceEndpointManager.cs @@ -5,6 +5,8 @@ namespace Microsoft.Azure.SignalR { + internal delegate void EndpointEventHandler(HubServiceEndpoint endpoint); + internal interface IServiceEndpointManager { IServiceEndpointProvider GetEndpointProvider(ServiceEndpoint endpoint); @@ -12,5 +14,11 @@ internal interface IServiceEndpointManager ServiceEndpoint[] Endpoints { get; } IReadOnlyList GetEndpoints(string hub); + + event EndpointEventHandler OnAdd; + + event EndpointEventHandler OnRemove; + + event EndpointEventHandler OnRename; } } diff --git a/src/Microsoft.Azure.SignalR.Common/ServiceConnections/MultiEndpointServiceConnectionContainer.cs b/src/Microsoft.Azure.SignalR.Common/ServiceConnections/MultiEndpointServiceConnectionContainer.cs index 739dc34dd..94d9c25e2 100644 --- a/src/Microsoft.Azure.SignalR.Common/ServiceConnections/MultiEndpointServiceConnectionContainer.cs +++ b/src/Microsoft.Azure.SignalR.Common/ServiceConnections/MultiEndpointServiceConnectionContainer.cs @@ -19,8 +19,10 @@ internal class MultiEndpointServiceConnectionContainer : IMultiEndpointServiceCo private readonly ConcurrentDictionary _connectionContainers = new ConcurrentDictionary(); + private readonly string _hubName; private readonly IMessageRouter _router; private readonly ILogger _logger; + private readonly IServiceEndpointManager _serviceEndpointManager; // private (bool needRouter, IReadOnlyList endpoints) _routerEndpoints; @@ -40,9 +42,11 @@ internal MultiEndpointServiceConnectionContainer( throw new ArgumentNullException(nameof(generator)); } + _hubName = hub; _router = router ?? throw new ArgumentNullException(nameof(router)); _logger = loggerFactory?.CreateLogger() ?? throw new ArgumentNullException(nameof(loggerFactory)); - + _serviceEndpointManager = endpointManager; + // provides a copy to the endpoint per container var endpoints = endpointManager.GetEndpoints(hub); // router will be used when there's customized MessageRouter or multiple endpoints @@ -53,7 +57,11 @@ internal MultiEndpointServiceConnectionContainer( foreach (var endpoint in endpoints) { _connectionContainers[endpoint] = generator(endpoint); - } + } + + _serviceEndpointManager.OnAdd += OnAdd; + _serviceEndpointManager.OnRemove += OnRemove; + _serviceEndpointManager.OnRename += OnRename; } public MultiEndpointServiceConnectionContainer( @@ -242,6 +250,51 @@ private Task WriteMultiEndpointMessageAsync(ServiceMessage serviceMessage, Func< return Task.WhenAll(routed); } + private void OnAdd(HubServiceEndpoint endpoint) + { + if (!endpoint.Hub.Equals(_hubName, StringComparison.OrdinalIgnoreCase)) + { + return; + } + _ = AddHubServiceEndpointAsync(endpoint); + } + + private Task AddHubServiceEndpointAsync(HubServiceEndpoint endpoint) + { + // TODO: create container and trigger server ping. + + // do tasks when !endpoint.ScaleTask.IsCanceled or local timeout check not finish + endpoint.CompleteScale(); + return Task.CompletedTask; + } + + private void OnRemove(HubServiceEndpoint endpoint) + { + if (!endpoint.Hub.Equals(_hubName, StringComparison.OrdinalIgnoreCase)) + { + return; + } + _ = RemoveHubServiceEndpointAsync(endpoint); + } + + private Task RemoveHubServiceEndpointAsync(HubServiceEndpoint endpoint) + { + // TODO: trigger offline ping and wait to remove container. + + // finally set task complete when timeout + endpoint.CompleteScale(); + return Task.CompletedTask; + } + + private void OnRename(HubServiceEndpoint endpoint) + { + if (!endpoint.Hub.Equals(_hubName, StringComparison.OrdinalIgnoreCase)) + { + return; + } + // TODO: update local store names + } + private static class Log { private static readonly Action _startingConnection = diff --git a/src/Microsoft.Azure.SignalR/EndpointProvider/ServiceEndpointManager.cs b/src/Microsoft.Azure.SignalR/EndpointProvider/ServiceEndpointManager.cs index 16bfa3b30..9ff415465 100644 --- a/src/Microsoft.Azure.SignalR/EndpointProvider/ServiceEndpointManager.cs +++ b/src/Microsoft.Azure.SignalR/EndpointProvider/ServiceEndpointManager.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -18,6 +19,7 @@ internal class ServiceEndpointManager : ServiceEndpointManagerBase // Store the initial ServiceOptions for generating EndpointProvider use. // Only Endpoints value accept hot-reload and prevent changes of unexpected modification on other configurations. private readonly ServiceOptions _options; + private readonly TimeSpan _scaleTimeout; private IReadOnlyList _endpointsStore; public ServiceEndpointManager(IOptionsMonitor optionsMonitor, ILoggerFactory loggerFactory) : @@ -33,6 +35,7 @@ public ServiceEndpointManager(IOptionsMonitor optionsMonitor, IL // TODO: Enable optionsMonitor.OnChange when feature ready. // optionsMonitor.OnChange(OnChange); _endpointsStore = Endpoints; + _scaleTimeout = _options.ServiceScaleTimeout; } public override IServiceEndpointProvider GetEndpointProvider(ServiceEndpoint endpoint) @@ -45,7 +48,7 @@ public override IServiceEndpointProvider GetEndpointProvider(ServiceEndpoint end return new ServiceEndpointProvider(endpoint, _options); } - private void OnChange(ServiceOptions options) + private async void OnChange(ServiceOptions options) { Log.DetectConfigurationChanges(_logger); @@ -60,6 +63,24 @@ private void OnChange(ServiceOptions options) var updatedEndpoints = GetChangedEndpoints(Endpoints); + await RenameSerivceEndpoints(updatedEndpoints.RenamedEndpoints); + + using (var addCts = new CancellationTokenSource(options.ServiceScaleTimeout)) + { + if (!await WaitTaskOrTimeout(AddServiceEndpointsAsync(updatedEndpoints.AddedEndpoints, addCts.Token), addCts)) + { + Log.TimeoutAddEndpoints(_logger); + } + } + + using (var removeCts = new CancellationTokenSource(options.ServiceScaleTimeout)) + { + if (!await WaitTaskOrTimeout(RemoveServiceEndpointsAsync(updatedEndpoints.RemovedEndpoints, removeCts.Token), removeCts)) + { + Log.TimeoutRemoveEndpoints(_logger); + } + } + _endpointsStore = Endpoints; } @@ -77,6 +98,19 @@ private void OnChange(ServiceOptions options) return (AddedEndpoints: addedEndpoints, RemovedEndpoints: removedEndpoints, RenamedEndpoints: renamedEndpoints); } + private static async Task WaitTaskOrTimeout(Task task, CancellationTokenSource cts) + { + var completed = await Task.WhenAny(task, Task.Delay(Timeout.InfiniteTimeSpan, cts.Token)); + + if (completed == task) + { + return true; + } + + cts.Cancel(); + return false; + } + private sealed class ServiceEndpointWeakComparer : IEqualityComparer { public bool Equals(ServiceEndpoint x, ServiceEndpoint y) @@ -96,7 +130,16 @@ private static class Log LoggerMessage.Define(LogLevel.Debug, new EventId(1, "DetectConfigurationChanges"), "Dected configuration changes in configuration, start live-scale."); private static readonly Action _endpointNotFound = - LoggerMessage.Define(LogLevel.Error, new EventId(1, "EndpointNotFound"), "No connection string is specified. Skip scale operation."); + LoggerMessage.Define(LogLevel.Warning, new EventId(2, "EndpointNotFound"), "No connection string is specified. Skip scale operation."); + + private static readonly Action _timeoutRenameEndpoints = + LoggerMessage.Define(LogLevel.Error, new EventId(3, "TimeoutRenameEndpoints"), "Timeout waiting for renaming endpoints."); + + private static readonly Action _timeoutAddEndpoints = + LoggerMessage.Define(LogLevel.Error, new EventId(4, "TimeoutAddEndpoints"), "Timeout waiting for adding endpoints."); + + private static readonly Action _timeoutRemoveEndpoints = + LoggerMessage.Define(LogLevel.Error, new EventId(5, "TimeoutRemoveEndpoints"), "Timeout waiting for removing endpoints."); public static void DetectConfigurationChanges(ILogger logger) { @@ -107,6 +150,21 @@ public static void EndpointNotFound(ILogger logger) { _endpointNotFound(logger, null); } + + public static void TimeoutRenameEndpoints(ILogger logger) + { + _timeoutRenameEndpoints(logger, null); + } + + public static void TimeoutAddEndpoints(ILogger logger) + { + _timeoutAddEndpoints(logger, null); + } + + public static void TimeoutRemoveEndpoints(ILogger logger) + { + _timeoutRemoveEndpoints(logger, null); + } } } } diff --git a/src/Microsoft.Azure.SignalR/ServiceOptions.cs b/src/Microsoft.Azure.SignalR/ServiceOptions.cs index 83c9099ca..c789a2a0b 100644 --- a/src/Microsoft.Azure.SignalR/ServiceOptions.cs +++ b/src/Microsoft.Azure.SignalR/ServiceOptions.cs @@ -75,6 +75,12 @@ public class ServiceOptions : IServiceEndpointOptions /// /// Gets or sets the proxy used when ServiceEndpoint will attempt to connect to Azure SignalR. /// - public IWebProxy Proxy { get; set; } + public IWebProxy Proxy { get; set; } + + /// + /// Gets or sets timeout waiting when scale multiple Azure SignalR Service endpoints. + /// Default value is 5 minutes + /// + internal TimeSpan ServiceScaleTimeout { get; set; } = TimeSpan.FromSeconds(Constants.DefaultScaleTimeoutInSeconds); } } From a0dc00b39f2e7b7078735a923054f85c435b7f54 Mon Sep 17 00:00:00 2001 From: JialinXin Date: Tue, 3 Mar 2020 13:16:21 +0800 Subject: [PATCH 07/14] Update rest-api doc. (#835) * update rest-api doc. * typo fix. --- docs/rest-api.md | 105 +++-- docs/swagger/v1.json | 918 ++++++++++++++++++++++++++++++++----------- 2 files changed, 766 insertions(+), 257 deletions(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index 692403086..7f77ee156 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -4,25 +4,28 @@ > > Azure SignalR Service only supports REST API for ASP.NET CORE SignalR applications. -- [REST API in Azure SignalR Service](#rest-api-in-azure-signalr-service) - - [Typical Server-less Architecture with Azure Functions](#typical-server-less-architecture-with-azure-functions) - - [API](#api) - - [Broadcast message to all clients](#broadcast-message-to-all-clients) - - [Broadcast message to a group](#broadcast-message-to-a-group) - - [Send message to a user](#send-message-to-a-user) - - [Add a connection to a group](#add-a-connection-to-a-group) - - [Remove a connection from a group](#remove-a-connection-from-a-group) - - [Add a user to a group](#add-a-user-to-a-group) - - [Remove a user from a group](#remove-a-user-from-a-group) - - [Remove a user from all groups](#remove-a-user-from-all-groups) - - [Check user existence in a group](#check-user-existence-in-a-group) - - [Using REST API](#using-rest-api) - - [Authentication](#authentication) - - [Signing Algorithm and Signature](#signing-algorithm-and-signature) - - [Claims](#claims) - - [Implement Negotiate Endpoint](#implement-negotiate-endpoint) - - [User-related REST API](#user-related-rest-api) - - [Sample](#sample) +- [REST API in Azure SignalR Service](#REST-API-in-Azure-SignalR-Service) + - [Typical Server-less Architecture with Azure Functions](#Typical-Server-less-Architecture-with-Azure-Functions) + - [API](#API) + - [Broadcast message to all clients](#Broadcast-message-to-all-clients) + - [Broadcast message to a group](#Broadcast-message-to-a-group) + - [Send message to a user](#Send-message-to-a-user) + - [Send message to a connection](#Send-message-to-a-connection) + - [Add a connection to a group](#Add-a-connection-to-a-group) + - [Remove a connection from a group](#Remove-a-connection-from-a-group) + - [Add a user to a group](#Add-a-user-to-a-group) + - [Remove a user from a group](#Remove-a-user-from-a-group) + - [Check user existence in a group](#Check-user-existence-in-a-group) + - [Remove a user from all groups](#Remove-a-user-from-all-groups) + - [Close a client connection](#Close-a-client-connection) + - [Service Health](#Service-Health) + - [Using REST API](#Using-REST-API) + - [Authentication](#Authentication) + - [Signing Algorithm and Signature](#Signing-Algorithm-and-Signature) + - [Claims](#Claims) + - [Implement Negotiate Endpoint](#Implement-Negotiate-Endpoint) + - [User-related REST API](#User-related-REST-API) + - [Sample](#Sample) On top of classical client-server pattern, Azure SignalR Service provides a set of REST APIs, so that you can easily integrate real-time functionality into your server-less architecture. @@ -60,9 +63,15 @@ API | `1.0-preview` | `1.0` Broadcast to a few groups | :heavy_check_mark: (Deprecated) | `N/A` [Send to a user](#send-user) | :heavy_check_mark: | :heavy_check_mark: Send to a few users | :heavy_check_mark: (Deprecated) | `N/A` +[Send to a connection](#Send-message-to-a-connection) | `N/A` | :heavy_check_mark: +[Add a connection to a group](#Add-a-connection-to-a-group) | `N/A` | :heavy_check_mark: +[Remove a connection from a group](#Remove-a-connection-from-a-group) | `N/A` | :heavy_check_mark: [Add a user to a group](#add-user-to-group) | `N/A` | :heavy_check_mark: [Remove a user from a group](#remove-user-from-group) | `N/A` | :heavy_check_mark: +[Check user existence](#Check-user-existence-in-a-group) | `N/A` | :heavy_check_mark: [Remove a user from all groups](#remove-user-from-all-groups) | `N/A` | :heavy_check_mark: +[Close a client connection](#Close-a-client-connection) | `N/A` | :heavy_check_mark: +[Service Health](#Service-Health) | `N/A` | :heavy_check_mark: @@ -74,6 +83,7 @@ API Version | HTTP Method | Request URL | Request Body `1.0` | `POST` | `https://.service.signalr.net/api/v1/hubs/` | Same as above + ### Broadcast message to a group API Version | HTTP Method | Request URL | Request Body @@ -82,6 +92,7 @@ API Version | HTTP Method | Request URL | Request Body `1.0` | `POST` | `https://.service.signalr.net/api/v1/hubs//groups/` | Same as above + ### Send message to a user API Version | HTTP Method | Request URL | Request Body @@ -89,7 +100,16 @@ API Version | HTTP Method | Request URL | Request Body `1.0-preview` | `POST` | `https://.service.signalr.net/api/v1-preview/hub//user/` | `{ "target":"", "arguments":[ ... ] }` `1.0` | `POST` | `https://.service.signalr.net/api/v1/hubs//users/` | Same as above + + +### Send message to a connection + +API Version | HTTP Method | Request URL | Request Body +---|---|---|--- +`1.0` | `POST` | `https://.service.signalr.net/api/v1/hubs//connections/` | `{ "target":"", "arguments":[ ... ] }` + + ### Add a connection to a group API Version | HTTP Method | Request URL @@ -98,6 +118,7 @@ API Version | HTTP Method | Request URL `1.0` | `PUT` | `https://.service.signalr.net/api/v1/hubs//connections//groups/` + ### Remove a connection from a group API Version | HTTP Method | Request URL @@ -106,6 +127,7 @@ API Version | HTTP Method | Request URL `1.0` | `DELETE` | `https://.service.signalr.net/api/v1/hubs//connections//groups/` + ### Add a user to a group API Version | HTTP Method | Request URL @@ -124,20 +146,49 @@ API Version | HTTP Method | Request URL `1.0` | `DELETE` | `https://.service.signalr.net/api/v1/hubs//groups//users/` `1.0` | `DELETE` | `https://.service.signalr.net/api/v1/hubs//users//groups/` + + +### Check user existence in a group + +API Version | HTTP Method | Request URL +---|---|--- +`1.0` | `GET` | `https://.service.signalr.net/api/v1/hubs//users//groups/` +`1.0` | `GET` | `https://.service.signalr.net/api/v1/hubs//groups//users/` + +Response Status Code | Description +---|--- +`200` | User exists +`404` | User not exists ### Remove a user from all groups -| API Version | HTTP Method | Request URL | -| ----------- | ----------- | ------------------------------------------------------------ | -| `1.0` | `DELETE` | `https://.service.signalr.net/api/v1/hubs//users//groups` | +API Version | HTTP Method | Request URL +---|---|--- +`1.0` | `DELETE` | `https://.service.signalr.net/api/v1/hubs//users//groups` -### Check user existence in a group -| API Version | HTTP Method | Request URL | -| ----------- | ----------- | ------------------------------------------------------------ | -| `1.0` | `GET` | `https://.service.signalr.net/api/v1/hubs//users//groups/` | -| `1.0` | `GET` | `https://.service.signalr.net/api/v1/hubs//groups//users/` | + + +### Close a client connection + +API Version | HTTP Method | Request URL +---|---|--- +`1.0` | `DELETE` | `https://.service.signalr.net/api/v1/hubs//connections/` +`1.0` | `DELETE` | `https://.service.signalr.net/api/v1/hubs//connections/?reason=` + + + +### Service Health + +API Version | HTTP Method | Request URL +---|---|--- +`1.0` | `GET` | `https://.service.signalr.net/api/v1/health` + +Response Status Code | Description +---|--- +`200` | Service Good +`503` | Service Unavailable ## Using REST API diff --git a/docs/swagger/v1.json b/docs/swagger/v1.json index f9cfea279..84edadd9b 100644 --- a/docs/swagger/v1.json +++ b/docs/swagger/v1.json @@ -1,252 +1,710 @@ { - "swagger": "2.0", + "openapi": "3.0.1", "info": { - "version": "v1", - "title": "Azure SignalR Service REST API" + "title": "Azure SignalR Service REST API", + "version": "v1" }, "paths": { - "/api/v1/hubs/{hub}/users/{id}": { - "post": { - "description": "Send a message to a single user.", - "tags": [], - "operationId": "SendMessageToUser", - "consumes": [ - "application/json" - ], - "produces": [], - "parameters": [ - { - "name": "hub", - "in": "path", - "required": true, - "type": "string", - "description": "Target hub name, which should start with alphabetic characters and only contain alpha-numeric characters or underscore." - }, - { - "name": "id", - "in": "path", - "required": true, - "type": "string", - "description": "Target user Id." - }, - { - "name": "message", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/Message" - } - } - ], - "responses": { - "202": { - "description": "Accepted" - } + "/api/v1/health": { + "get": { + "description": "Get service health status", + "tags": [], + "responses": { + "200": { + "description": "Ok" + }, + "503": { + "description": "ServiceUnavailable" + } + } + } + }, + "/api/v1/hubs/{hub}": { + "post": { + "description": "Broadcast a message to all clients connected to target hub.", + "tags": [], + "parameters": [ + { + "name": "hub", + "in": "path", + "required": true, + "description": "Target hub name, which should start with alphabetic characters and only contain alpha-numeric characters or underscore.", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadMessage" } + } } - }, - "/api/v1/hubs/{hub}": { - "post": { - "description": "Broadcast a message to all clients connected to target hub.", - "tags": [], - "operationId": "BroadcastMessage", - "consumes": [ - "application/json" - ], - "produces": [], - "parameters": [ - { - "name": "hub", - "in": "path", - "required": true, - "type": "string", - "description": "Target hub name, which should start with alphabetic characters and only contain alpha-numeric characters or underscore." - }, - { - "name": "message", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/Message" - } - } - ], - "responses": { - "202": { - "description": "Accepted" - } + }, + "responses": { + "202": { + "description": "Accepted" + } + } + } + }, + "/api/v1/hubs/{hub}/users/{id}": { + "post": { + "description": "Broadcast a message to all clients within the target group.", + "tags": [], + "parameters": [ + { + "name": "hub", + "in": "path", + "required": true, + "description": "Target hub name, which should start with alphabetic characters and only contain alpha-numeric characters or underscore.", + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "description": "Target user Id.", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadMessage" } + } } - }, - "/api/v1/hubs/{hub}/groups/{group}": { - "post": { - "description": "Broadcast a message to all clients within the target group.", - "tags": [], - "operationId": "SendMessageToGroup", - "consumes": [ - "application/json" - ], - "produces": [], - "parameters": [ - { - "name": "hub", - "in": "path", - "required": true, - "type": "string", - "description": "Target hub name, which should start with alphabetic characters and only contain alpha-numeric characters or underscore." - }, - { - "name": "group", - "in": "path", - "required": true, - "type": "string", - "description": "Target group name, which should start with alphabetic characters and only contain alpha-numeric characters or underscore." - }, - { - "name": "message", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/Message" - } - } - ], - "responses": { - "202": { - "description": "Accepted" - } + }, + "responses": { + "202": { + "description": "Accepted" + } + } + } + }, + "/api/v1/hubs/{hub}/connections/{connectionId}": { + "post": { + "description": "Send message to a single connection Id.", + "tags": [], + "parameters": [ + { + "name": "hub", + "in": "path", + "required": true, + "description": "Target hub name, which should start with alphabetic characters and only contain alpha-numeric characters or underscore.", + "schema": { + "type": "string" + } + }, + { + "name": "connectionId", + "in": "path", + "required": true, + "description": "Target connection Id.", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadMessage" } + } } + }, + "responses": { + "202": { + "description": "Accepted" + } + } }, - "/api/v1/hubs/{hub}/groups/{group}/users/{id}": { - "put": { - "description": "Add a user to the target group.", - "tags": [], - "operationId": "AddUserToGroup", - "consumes": [ - "application/json" - ], - "produces": [], - "parameters": [ - { - "name": "hub", - "in": "path", - "required": true, - "type": "string", - "description": "Target hub name, which should start with alphabetic characters and only contain alpha-numeric characters or underscore." - }, - { - "name": "group", - "in": "path", - "required": true, - "type": "string", - "description": "Target group name, which should start with alphabetic characters and only contain alpha-numeric characters or underscore." - }, - { - "name": "id", - "in": "path", - "required": true, - "type": "string", - "description": "Target user Id." - } - ], - "responses": { - "202": { - "description": "Accepted" - } - } + "delete": { + "description": "Close a target client connection Id with reason.", + "tags": [], + "parameters": [ + { + "name": "hub", + "in": "path", + "required": true, + "description": "Target hub name, which should start with alphabetic characters and only contain alpha-numeric characters or underscore.", + "schema": { + "type": "string" + } + }, + { + "name": "connectionId", + "in": "path", + "required": true, + "description": "Target connection Id.", + "schema": { + "type": "string" + } }, - "delete": { - "description": "Remove a user from the target group.", - "tags": [], - "operationId": "RemoveUserFromGroup", - "consumes": [ - "application/json" - ], - "produces": [], - "parameters": [ - { - "name": "hub", - "in": "path", - "required": true, - "type": "string", - "description": "Target hub name, which should start with alphabetic characters and only contain alpha-numeric characters or underscore." - }, - { - "name": "group", - "in": "path", - "required": true, - "type": "string", - "description": "Target group name, which should start with alphabetic characters and only contain alpha-numeric characters or underscore." - }, - { - "name": "id", - "in": "path", - "required": true, - "type": "string", - "description": "Target user Id." - } - ], - "responses": { - "202": { - "description": "Accepted" - } + { + "name": "reason", + "in": "query", + "description": "Close reason.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "202": { + "description": "Accepted" + } + } + } + }, + "/api/v1/hubs/{hub}/groups/{group}": { + "post": { + "description": "Broadcast a message to all clients within the target group.", + "tags": [], + "parameters": [ + { + "name": "hub", + "in": "path", + "required": true, + "description": "Target hub name, which should start with alphabetic characters and only contain alpha-numeric characters or underscore.", + "schema": { + "type": "string" + } + }, + { + "name": "group", + "in": "path", + "required": true, + "description": "Target group name, which length should be greater than 0 and less than 1025.", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadMessage" } + } } + }, + "responses": { + "202": { + "description": "Ok" + } + } + } + }, + "/api/v1/hubs/{hub}/groups/{group}/users/{user}": { + "get": { + "description": "Check whether a user exists in the target group.", + "tags": [], + "parameters": [ + { + "name": "hub", + "in": "path", + "required": true, + "description": "Target hub name, which should start with alphabetic characters and only contain alpha-numeric characters or underscore.", + "schema": { + "type": "string" + } + }, + { + "name": "group", + "in": "path", + "required": true, + "description": "Target group name, which length should be greater than 0 and less than 1025.", + "schema": { + "type": "string" + } + }, + { + "name": "user", + "in": "path", + "required": true, + "description": "Target user Id.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Ok" + }, + "404": { + "description": "NotFound" + } + } }, - "/api/v1/hubs/{hub}/users/{id}/groups": { - "delete": { - "description": "Remove a user from all groups.", - "tags": [], - "operationId": "RemoveUserFromAllGroups", - "consumes": [ - "application/json" - ], - "produces": [], - "parameters": [ - { - "name": "hub", - "in": "path", - "required": true, - "type": "string", - "description": "Target hub name, which should start with alphabetic characters and only contain alpha-numeric characters or underscore." - }, - { - "name": "id", - "in": "path", - "required": true, - "type": "string", - "description": "Target user Id." - } - ], - "responses": { - "202": { - "description": "Accepted" - }, - "200": { - "description": "Ok" - } - } + "put": { + "description": "Add a user to the target group.", + "tags": [], + "parameters": [ + { + "name": "hub", + "in": "path", + "required": true, + "description": "Target hub name, which should start with alphabetic characters and only contain alpha-numeric characters or underscore.", + "schema": { + "type": "string" + } + }, + { + "name": "group", + "in": "path", + "required": true, + "description": "Target group name, which length should be greater than 0 and less than 1025.", + "schema": { + "type": "string" + } + }, + { + "name": "user", + "in": "path", + "required": true, + "description": "Target user Id.", + "schema": { + "type": "string" + } + }, + { + "name": "ttl", + "in": "query", + "description": "Time to live in seconds for the user Id. Optional for live forever.", + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "202": { + "description": "Accepted" + } + } + }, + "delete": { + "description": "Remove a user from the target group.", + "tags": [], + "parameters": [ + { + "name": "hub", + "in": "path", + "required": true, + "description": "Target hub name, which should start with alphabetic characters and only contain alpha-numeric characters or underscore.", + "schema": { + "type": "string" + } + }, + { + "name": "group", + "in": "path", + "required": true, + "description": "Target group name, which length should be greater than 0 and less than 1025.", + "schema": { + "type": "string" + } + }, + { + "name": "user", + "in": "path", + "required": true, + "description": "Target user Id.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "202": { + "description": "Accepted" + } + } + } + }, + "/api/v1/hubs/{hub}/users/{user}/groups/{group}": { + "get": { + "description": "Check whether a user exists in the target group.", + "tags": [], + "parameters": [ + { + "name": "hub", + "in": "path", + "required": true, + "description": "Target hub name, which should start with alphabetic characters and only contain alpha-numeric characters or underscore.", + "schema": { + "type": "string" + } + }, + { + "name": "group", + "in": "path", + "required": true, + "description": "Target group name, which length should be greater than 0 and less than 1025.", + "schema": { + "type": "string" + } + }, + { + "name": "user", + "in": "path", + "required": true, + "description": "Target user Id.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Ok" + }, + "404": { + "description": "NotFound" + } + } + }, + "put": { + "description": "Add a user to the target group.", + "tags": [], + "parameters": [ + { + "name": "hub", + "in": "path", + "required": true, + "description": "Target hub name, which should start with alphabetic characters and only contain alpha-numeric characters or underscore.", + "schema": { + "type": "string" + } + }, + { + "name": "group", + "in": "path", + "required": true, + "description": "Target group name, which length should be greater than 0 and less than 1025.", + "schema": { + "type": "string" + } + }, + { + "name": "user", + "in": "path", + "required": true, + "description": "Target user Id.", + "schema": { + "type": "string" + } + }, + { + "name": "ttl", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "202": { + "description": "Accepted" + } + } + }, + "delete": { + "description": "Remove a user from the target group.", + "tags": [], + "parameters": [ + { + "name": "hub", + "in": "path", + "required": true, + "description": "Target hub name, which should start with alphabetic characters and only contain alpha-numeric characters or underscore.", + "schema": { + "type": "string" + } + }, + { + "name": "group", + "in": "path", + "required": true, + "description": "Target group name, which length should be greater than 0 and less than 1025.", + "schema": { + "type": "string" + } + }, + { + "name": "user", + "in": "path", + "required": true, + "description": "Target user Id.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "202": { + "description": "Accepted" + } + } + } + }, + "/api/v1/hubs/{hub}/groups/{group}/connections/{connectionId}": { + "put": { + "description": "Add a connection to the target group.", + "tags": [], + "parameters": [ + { + "name": "hub", + "in": "path", + "required": true, + "description": "Target hub name, which should start with alphabetic characters and only contain alpha-numeric characters or underscore.", + "schema": { + "type": "string" + } + }, + { + "name": "group", + "in": "path", + "required": true, + "description": "Target group name, which length should be greater than 0 and less than 1025.", + "schema": { + "type": "string" + } + }, + { + "name": "connectionId", + "in": "path", + "required": true, + "description": "Target connection Id.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "202": { + "description": "Accepted" + } + } + }, + "delete": { + "description": "Remove a connection from the target group.", + "tags": [], + "parameters": [ + { + "name": "hub", + "in": "path", + "required": true, + "description": "Target hub name, which should start with alphabetic characters and only contain alpha-numeric characters or underscore.", + "schema": { + "type": "string" + } + }, + { + "name": "group", + "in": "path", + "required": true, + "description": "Target group name, which length should be greater than 0 and less than 1025.", + "schema": { + "type": "string" + } + }, + { + "name": "connectionId", + "in": "path", + "required": true, + "description": "Target connection Id.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "202": { + "description": "Accepted" } + } } + }, + "/api/v1/hubs/{hub}/connections/{connectionId}/groups/{group}": { + "put": { + "description": "Add a connection to the target group.", + "tags": [], + "parameters": [ + { + "name": "hub", + "in": "path", + "required": true, + "description": "Target hub name, which should start with alphabetic characters and only contain alpha-numeric characters or underscore.", + "schema": { + "type": "string" + } + }, + { + "name": "group", + "in": "path", + "required": true, + "description": "Target group name, which length should be greater than 0 and less than 1025.", + "schema": { + "type": "string" + } + }, + { + "name": "connectionId", + "in": "path", + "required": true, + "description": "Target connection Id.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "202": { + "description": "Accepted" + } + } + }, + "delete": { + "description": "Remove a connection from the target group.", + "tags": [], + "parameters": [ + { + "name": "hub", + "in": "path", + "required": true, + "description": "Target hub name, which should start with alphabetic characters and only contain alpha-numeric characters or underscore.", + "schema": { + "type": "string" + } + }, + { + "name": "group", + "in": "path", + "required": true, + "description": "Target group name, which length should be greater than 0 and less than 1025.", + "schema": { + "type": "string" + } + }, + { + "name": "connectionId", + "in": "path", + "required": true, + "description": "Target connection Id.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "202": { + "description": "Accepted" + } + } + } + }, + "/api/v1/hubs/{hub}/users/{user}/groups": { + "delete": { + "description": "Remove a user from all groups.", + "tags": [], + "parameters": [ + { + "name": "hub", + "in": "path", + "required": true, + "description": "Target hub name, which should start with alphabetic characters and only contain alpha-numeric characters or underscore.", + "schema": { + "type": "string" + } + }, + { + "name": "user", + "in": "path", + "required": true, + "description": "Target user Id.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "202": { + "description": "Accepted" + } + } + } + } }, - "definitions": { - "Message": { - "type": "object", - "description": "Method invocation message.", - "properties": { - "target": { - "type": "string", - "description": "Target method name." - }, - "arguments": { - "type": "array", - "items": { - "type": "object" - }, - "description": "Target method arguments." + "components": { + "schemas": { + "IActionResult": { + "type": "object", + "additionalProperties": false + }, + "IActionResultValueTask": { + "type": "object", + "properties": { + "IsCompleted": { + "type": "boolean", + "readOnly": true + }, + "IsCompletedSuccessfully": { + "type": "boolean", + "readOnly": true + }, + "IsFaulted": { + "type": "boolean", + "readOnly": true + }, + "IsCanceled": { + "type": "boolean", + "readOnly": true + }, + "Result": { + "allOf": [ + { + "$ref": "#/components/schemas/IActionResult" } + ], + "nullable": true, + "readOnly": true + } + }, + "additionalProperties": false + }, + "PayloadMessage": { + "type": "object", + "properties": { + "Target": { + "type": "string", + "nullable": true + }, + "Arguments": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false + }, + "nullable": true } + }, + "additionalProperties": false } + } } -} + } \ No newline at end of file From f3dc9d40cccdae2e8ac2f2afd56894a5467cfb00 Mon Sep 17 00:00:00 2001 From: JialinXin Date: Wed, 4 Mar 2020 11:25:01 +0800 Subject: [PATCH 08/14] improve log for rebalance connection setup (#838) --- .../ServiceConnections/ServiceConnectionBase.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Azure.SignalR.Common/ServiceConnections/ServiceConnectionBase.cs b/src/Microsoft.Azure.SignalR.Common/ServiceConnections/ServiceConnectionBase.cs index 2d5f87f0e..22402fccf 100644 --- a/src/Microsoft.Azure.SignalR.Common/ServiceConnections/ServiceConnectionBase.cs +++ b/src/Microsoft.Azure.SignalR.Common/ServiceConnections/ServiceConnectionBase.cs @@ -316,7 +316,12 @@ private async Task EstablishConnectionAsync(string target) } catch (Exception ex) { - Log.FailedToConnect(Logger, HubEndpoint.ToString(), ConnectionId, ex); + if (target == null) + { + // Log for required connections only to reduce noise for rebalance + // connection failure usually due to service maintenance. + Log.FailedToConnect(Logger, HubEndpoint.ToString(), ConnectionId, ex); + } return null; } } From 7c3b91bea23e1b1ccb033963b052a2d0a78a00ba Mon Sep 17 00:00:00 2001 From: JialinXin Date: Wed, 4 Mar 2020 13:40:05 +0800 Subject: [PATCH 09/14] Move container into HubServiceEndpoint (#837) * move container into HubServiceEndpoint * minor improve * minor fix namespace and UT * add log for endpoint not exist * minor fix selection logic --- .../Endpoints/HubServiceEndpoint.cs | 2 + ...MultiEndpointServiceConnectionContainer.cs | 47 ++++++++----------- .../WeakServiceConnectionContainer.cs | 4 +- .../ServiceHubContext.cs | 2 +- .../ServiceManager.cs | 2 +- ...EndpointServiceConnectionContainerTests.cs | 1 - .../ServiceConnectionContainerBaseTests.cs | 2 +- .../WeakServiceConnectionContainerTests.cs | 2 +- ...EndpointServiceConnectionContainerTests.cs | 4 +- 9 files changed, 28 insertions(+), 38 deletions(-) diff --git a/src/Microsoft.Azure.SignalR.Common/Endpoints/HubServiceEndpoint.cs b/src/Microsoft.Azure.SignalR.Common/Endpoints/HubServiceEndpoint.cs index 8df4bede2..649e72227 100644 --- a/src/Microsoft.Azure.SignalR.Common/Endpoints/HubServiceEndpoint.cs +++ b/src/Microsoft.Azure.SignalR.Common/Endpoints/HubServiceEndpoint.cs @@ -27,6 +27,8 @@ internal HubServiceEndpoint() : base() { } public IServiceEndpointProvider Provider { get; } + public IServiceConnectionContainer ConnectionContainer { get; set; } + /// /// Task waiting for HubServiceEndpoint turn ready when live add/remove endpoint /// diff --git a/src/Microsoft.Azure.SignalR.Common/ServiceConnections/MultiEndpointServiceConnectionContainer.cs b/src/Microsoft.Azure.SignalR.Common/ServiceConnections/MultiEndpointServiceConnectionContainer.cs index 94d9c25e2..14b4057e8 100644 --- a/src/Microsoft.Azure.SignalR.Common/ServiceConnections/MultiEndpointServiceConnectionContainer.cs +++ b/src/Microsoft.Azure.SignalR.Common/ServiceConnections/MultiEndpointServiceConnectionContainer.cs @@ -1,24 +1,19 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System; -using System.Collections.Concurrent; +using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Azure.SignalR.Common; -using Microsoft.Azure.SignalR.Common.ServiceConnections; using Microsoft.Azure.SignalR.Protocol; using Microsoft.Extensions.Logging; namespace Microsoft.Azure.SignalR { internal class MultiEndpointServiceConnectionContainer : IMultiEndpointServiceConnectionContainer - { - private readonly ConcurrentDictionary _connectionContainers = - new ConcurrentDictionary(); - + { private readonly string _hubName; private readonly IMessageRouter _router; private readonly ILogger _logger; @@ -27,9 +22,6 @@ internal class MultiEndpointServiceConnectionContainer : IMultiEndpointServiceCo // private (bool needRouter, IReadOnlyList endpoints) _routerEndpoints; - // for test use - public IReadOnlyDictionary ConnectionContainers => _connectionContainers; - internal MultiEndpointServiceConnectionContainer( string hub, Func generator, @@ -56,7 +48,7 @@ internal MultiEndpointServiceConnectionContainer( foreach (var endpoint in endpoints) { - _connectionContainers[endpoint] = generator(endpoint); + endpoint.ConnectionContainer = generator(endpoint); } _serviceEndpointManager.OnAdd += OnAdd; @@ -83,7 +75,7 @@ ILoggerFactory loggerFactory public IEnumerable GetOnlineEndpoints() { - return _connectionContainers.Where(s => s.Key.Online).Select(s => s.Key); + return _routerEndpoints.endpoints.Where(s => s.Online); } private static IServiceConnectionContainer CreateContainer(IServiceConnectionFactory serviceConnectionFactory, HubServiceEndpoint endpoint, int count, ILoggerFactory loggerFactory) @@ -104,8 +96,8 @@ public Task ConnectionInitializedTask { get { - return Task.WhenAll(from connection in _connectionContainers - select connection.Value.ConnectionInitializedTask); + return Task.WhenAll(from connection in _routerEndpoints.endpoints + select connection.ConnectionContainer.ConnectionInitializedTask); } } @@ -115,25 +107,25 @@ public Task ConnectionInitializedTask public Task StartAsync() { - return Task.WhenAll(_connectionContainers.Select(s => + return Task.WhenAll(_routerEndpoints.endpoints.Select(s => { - Log.StartingConnection(_logger, s.Key.Endpoint); - return s.Value.StartAsync(); + Log.StartingConnection(_logger, s.Endpoint); + return s.ConnectionContainer.StartAsync(); })); } public Task StopAsync() { - return Task.WhenAll(_connectionContainers.Select(s => + return Task.WhenAll(_routerEndpoints.endpoints.Select(s => { - Log.StoppingConnection(_logger, s.Key.Endpoint); - return s.Value.StopAsync(); + Log.StoppingConnection(_logger, s.Endpoint); + return s.ConnectionContainer.StopAsync(); })); } public Task OfflineAsync(bool migratable) { - return Task.WhenAll(_connectionContainers.Select(c => c.Value.OfflineAsync(migratable))); + return Task.WhenAll(_routerEndpoints.endpoints.Select(c => c.ConnectionContainer.OfflineAsync(migratable))); } public Task WriteAsync(ServiceMessage serviceMessage) @@ -168,12 +160,12 @@ public async Task WriteAckableMessageAsync(ServiceMessage serviceMessage, public Task StartGetServersPing() { - return Task.WhenAll(ConnectionContainers.Select(c => c.Value.StartGetServersPing())); + return Task.WhenAll(_routerEndpoints.endpoints.Select(c => c.ConnectionContainer.StartGetServersPing())); } public Task StopGetServersPing() { - return Task.WhenAll(ConnectionContainers.Select(c => c.Value.StopGetServersPing())); + return Task.WhenAll(_routerEndpoints.endpoints.Select(c => c.ConnectionContainer.StopGetServersPing())); } internal IEnumerable GetRoutedEndpoints(ServiceMessage message) @@ -213,13 +205,12 @@ private Task WriteMultiEndpointMessageAsync(ServiceMessage serviceMessage, Func< var routed = GetRoutedEndpoints(serviceMessage)? .Select(endpoint => { - if (_connectionContainers.TryGetValue(endpoint, out var connection)) + var connection = (endpoint as HubServiceEndpoint)?.ConnectionContainer; + if (connection == null) { - return (e: endpoint, c: connection); + Log.EndpointNotExists(_logger, endpoint.ToString()); } - - Log.EndpointNotExists(_logger, endpoint.ToString()); - return (e: endpoint, c: null); + return (e: endpoint, c: connection); }) .Where(c => c.c != null) .Select(async s => diff --git a/src/Microsoft.Azure.SignalR.Common/ServiceConnections/WeakServiceConnectionContainer.cs b/src/Microsoft.Azure.SignalR.Common/ServiceConnections/WeakServiceConnectionContainer.cs index c5a15f390..3a97697fd 100644 --- a/src/Microsoft.Azure.SignalR.Common/ServiceConnections/WeakServiceConnectionContainer.cs +++ b/src/Microsoft.Azure.SignalR.Common/ServiceConnections/WeakServiceConnectionContainer.cs @@ -2,13 +2,11 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; -using System.Diagnostics; -using System.Threading; using System.Threading.Tasks; using Microsoft.Azure.SignalR.Protocol; using Microsoft.Extensions.Logging; -namespace Microsoft.Azure.SignalR.Common.ServiceConnections +namespace Microsoft.Azure.SignalR.Common { internal class WeakServiceConnectionContainer : ServiceConnectionContainerBase { diff --git a/src/Microsoft.Azure.SignalR.Management/ServiceHubContext.cs b/src/Microsoft.Azure.SignalR.Management/ServiceHubContext.cs index 82dc6f9a8..798e0757a 100644 --- a/src/Microsoft.Azure.SignalR.Management/ServiceHubContext.cs +++ b/src/Microsoft.Azure.SignalR.Management/ServiceHubContext.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR; -using Microsoft.Azure.SignalR.Common.ServiceConnections; +using Microsoft.Azure.SignalR.Common; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.Azure.SignalR.Management diff --git a/src/Microsoft.Azure.SignalR.Management/ServiceManager.cs b/src/Microsoft.Azure.SignalR.Management/ServiceManager.cs index 74401d6f3..c61db7e36 100644 --- a/src/Microsoft.Azure.SignalR.Management/ServiceManager.cs +++ b/src/Microsoft.Azure.SignalR.Management/ServiceManager.cs @@ -9,7 +9,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.SignalR; -using Microsoft.Azure.SignalR.Common.ServiceConnections; +using Microsoft.Azure.SignalR.Common; using Microsoft.Azure.SignalR.Protocol; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; diff --git a/test/Microsoft.Azure.SignalR.AspNet.Tests/MultiEndpointServiceConnectionContainerTests.cs b/test/Microsoft.Azure.SignalR.AspNet.Tests/MultiEndpointServiceConnectionContainerTests.cs index 6ef024e04..72f2eb86f 100644 --- a/test/Microsoft.Azure.SignalR.AspNet.Tests/MultiEndpointServiceConnectionContainerTests.cs +++ b/test/Microsoft.Azure.SignalR.AspNet.Tests/MultiEndpointServiceConnectionContainerTests.cs @@ -202,7 +202,6 @@ public async Task TestEndpointManagerWithDuplicateEndpointsAndConnectionStarted( Assert.Equal(2, endpoints.Length); Assert.Equal("1", endpoints[0].Name); Assert.Equal("11", endpoints[1].Name); - Assert.Equal(2, container.ConnectionContainers.Count); } [Fact] diff --git a/test/Microsoft.Azure.SignalR.Common.Tests/ServiceConnectionContainerBaseTests.cs b/test/Microsoft.Azure.SignalR.Common.Tests/ServiceConnectionContainerBaseTests.cs index 57cae7515..25980533e 100644 --- a/test/Microsoft.Azure.SignalR.Common.Tests/ServiceConnectionContainerBaseTests.cs +++ b/test/Microsoft.Azure.SignalR.Common.Tests/ServiceConnectionContainerBaseTests.cs @@ -2,7 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; -using Microsoft.Azure.SignalR.Common.ServiceConnections; +using Microsoft.Azure.SignalR.Common; using Microsoft.Azure.SignalR.Tests.Common; using Microsoft.Extensions.Logging; using Xunit; diff --git a/test/Microsoft.Azure.SignalR.Common.Tests/WeakServiceConnectionContainerTests.cs b/test/Microsoft.Azure.SignalR.Common.Tests/WeakServiceConnectionContainerTests.cs index fb95201c1..10a0a1c7c 100644 --- a/test/Microsoft.Azure.SignalR.Common.Tests/WeakServiceConnectionContainerTests.cs +++ b/test/Microsoft.Azure.SignalR.Common.Tests/WeakServiceConnectionContainerTests.cs @@ -2,7 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; -using Microsoft.Azure.SignalR.Common.ServiceConnections; +using Microsoft.Azure.SignalR.Common; using Microsoft.Extensions.Logging.Abstractions; using Xunit; diff --git a/test/Microsoft.Azure.SignalR.Tests/MultiEndpointServiceConnectionContainerTests.cs b/test/Microsoft.Azure.SignalR.Tests/MultiEndpointServiceConnectionContainerTests.cs index b31878368..9aeacee40 100644 --- a/test/Microsoft.Azure.SignalR.Tests/MultiEndpointServiceConnectionContainerTests.cs +++ b/test/Microsoft.Azure.SignalR.Tests/MultiEndpointServiceConnectionContainerTests.cs @@ -81,7 +81,8 @@ public void TestEndpointManagerWithDuplicateEndpoints() new TestSimpleServiceConnection(), }, e), sem, router, NullLoggerFactory.Instance); - Assert.Equal(2, container.ConnectionContainers.Count); + var containerEndpoints = container.GetOnlineEndpoints(); + Assert.Equal(2, containerEndpoints.Count()); } [Fact] @@ -113,7 +114,6 @@ public async Task TestEndpointManagerWithDuplicateEndpointsAndConnectionStarted( Assert.Equal(2, endpoints.Length); Assert.Equal("1", endpoints[0].Name); Assert.Equal("11", endpoints[1].Name); - Assert.Equal(2, container.ConnectionContainers.Count); } [Fact] From 1f84a02a7da7ec2f663d7d8b557fcf478dbb8d97 Mon Sep 17 00:00:00 2001 From: JialinXin Date: Fri, 13 Mar 2020 15:25:10 +0800 Subject: [PATCH 10/14] add doc about ping support. (#843) --- specs/ServiceProtocol.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/specs/ServiceProtocol.md b/specs/ServiceProtocol.md index ce7126c18..5e3e07d47 100644 --- a/specs/ServiceProtocol.md +++ b/specs/ServiceProtocol.md @@ -95,6 +95,22 @@ Service supports various scenarios in SignalR to send data from Server to multip - When Server wants to send data to a specific group, a `GroupBroadcastData` message is sent to Service. - When Server wants to send data to a couple of groups, a `MultiGroupBroadcastData` message is sent to Service. +### SignalR service pings + +Service enable pings for various scenarios of feature support and status sync between server and service. + +Key | Value | Direction | Description +---|---|---|--- +EMPTY | EMPTY | Both | Keep server connection alive ping +`target` | `` | Service -> Server | Rebalance ping to request server connection to improve availability +`status` | EMPTY | Server -> Service | Request to know whether service has clients +`status` | `0` or `1` | Service -> Server | Response to `status` ping of whether service has clients +`offline` | `fin:0` | Server -> Service | Request to drop clients for non-migratable server connections +`offline` | `fin:1` | Server -> Service | Request to migrate client connections +`offline` | `finack` | Service -> Server | Response of received `offline` request +`servers` | EMPTY | Server -> Service | Request to get all server ids connect to the service +`servers` | `:;` | Service -> Server | Response of `servers` ping of all server ids + ## Message Encodings In Azure SignalR Service Protocol, each message is represented as a single MessagePack array containing items that correspond to properties of the given service message. @@ -132,9 +148,10 @@ MessagePack uses different formats to encode values. Refer to the [MessagePack F ### Ping Message `Ping` messages have the following structure. ``` -[3] +[3, Messages] ``` - 3 - Message Type, indicating this is a `Ping` message. +- Messages - An `Array` of `String` indicates `Ping` message type and value. #### Example: TODO From 7d63500842fd16f38a126323e44455a07d355dca Mon Sep 17 00:00:00 2001 From: JialinXin Date: Fri, 13 Mar 2020 16:11:47 +0800 Subject: [PATCH 11/14] [Part3.3][Live-scale] Implement rename endpoint. (#836) * implement rename endpoint. * remove unused lock * merge change. * enable name internal set to simplify update name. * simplify rename * refactor Endpoints * resolve comments. --- .../ServiceEndpointManager.cs | 5 - .../Endpoints/HubServiceEndpoint.cs | 10 +- .../Endpoints/ScaleOperation.cs | 12 ++ .../Endpoints/ServiceEndpoint.cs | 2 +- .../Endpoints/ServiceEndpointManagerBase.cs | 184 +++++++++++++----- ...SignalRConfigurationNoEndpointException.cs | 21 ++ .../Interfaces/IServiceEndpointManager.cs | 4 +- ...MultiEndpointServiceConnectionContainer.cs | 14 +- .../ServiceEndpointManager.cs | 116 +---------- ...EndpointServiceConnectionContainerTests.cs | 10 +- .../RunAzureSignalRTests.cs | 23 +-- .../AddAzureSignalRFacts.cs | 7 +- .../AzureSignalRMarkerServiceFact.cs | 3 +- .../Infrastructure/ServiceConnectionProxy.cs | 2 +- ...EndpointServiceConnectionContainerTests.cs | 170 +++++++++++++++- 15 files changed, 370 insertions(+), 213 deletions(-) create mode 100644 src/Microsoft.Azure.SignalR.Common/Endpoints/ScaleOperation.cs create mode 100644 src/Microsoft.Azure.SignalR.Common/Exceptions/AzureSignalRConfigurationNoEndpointException.cs diff --git a/src/Microsoft.Azure.SignalR.AspNet/EndpointProvider/ServiceEndpointManager.cs b/src/Microsoft.Azure.SignalR.AspNet/EndpointProvider/ServiceEndpointManager.cs index 25cd1ee62..cda0ad849 100644 --- a/src/Microsoft.Azure.SignalR.AspNet/EndpointProvider/ServiceEndpointManager.cs +++ b/src/Microsoft.Azure.SignalR.AspNet/EndpointProvider/ServiceEndpointManager.cs @@ -14,11 +14,6 @@ public ServiceEndpointManager(ServiceOptions options, ILoggerFactory loggerFacto base(options, loggerFactory?.CreateLogger()) { - if (Endpoints.Length == 0) - { - throw new ArgumentException(ServiceEndpointProvider.ConnectionStringNotFound); - } - _options = options; } diff --git a/src/Microsoft.Azure.SignalR.Common/Endpoints/HubServiceEndpoint.cs b/src/Microsoft.Azure.SignalR.Common/Endpoints/HubServiceEndpoint.cs index 649e72227..c6839cba8 100644 --- a/src/Microsoft.Azure.SignalR.Common/Endpoints/HubServiceEndpoint.cs +++ b/src/Microsoft.Azure.SignalR.Common/Endpoints/HubServiceEndpoint.cs @@ -8,6 +8,7 @@ namespace Microsoft.Azure.SignalR internal class HubServiceEndpoint : ServiceEndpoint { private readonly TaskCompletionSource _scaleTcs; + private readonly ServiceEndpoint _endpoint; public HubServiceEndpoint( string hub, @@ -18,13 +19,20 @@ public HubServiceEndpoint( { Hub = hub; Provider = provider; + _endpoint = endpoint; _scaleTcs = needScaleTcs ? new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously) : null; } - internal HubServiceEndpoint() : base() { } + // for tests + internal HubServiceEndpoint() : base() + { + _endpoint = new ServiceEndpoint(); + } public string Hub { get; } + public override string Name => _endpoint.Name; + public IServiceEndpointProvider Provider { get; } public IServiceConnectionContainer ConnectionContainer { get; set; } diff --git a/src/Microsoft.Azure.SignalR.Common/Endpoints/ScaleOperation.cs b/src/Microsoft.Azure.SignalR.Common/Endpoints/ScaleOperation.cs new file mode 100644 index 000000000..38989e3a2 --- /dev/null +++ b/src/Microsoft.Azure.SignalR.Common/Endpoints/ScaleOperation.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Azure.SignalR +{ + internal enum ScaleOperation + { + Add, + Remove, + Rename + } +} diff --git a/src/Microsoft.Azure.SignalR.Common/Endpoints/ServiceEndpoint.cs b/src/Microsoft.Azure.SignalR.Common/Endpoints/ServiceEndpoint.cs index 6373b908c..bf9eb8083 100644 --- a/src/Microsoft.Azure.SignalR.Common/Endpoints/ServiceEndpoint.cs +++ b/src/Microsoft.Azure.SignalR.Common/Endpoints/ServiceEndpoint.cs @@ -12,7 +12,7 @@ public class ServiceEndpoint public EndpointType EndpointType { get; } - public string Name { get; } + public virtual string Name { get; internal set; } /// /// Initial status as Online so that when the app server first starts, it can accept incoming negotiate requests, as for backward compatability diff --git a/src/Microsoft.Azure.SignalR.Common/Endpoints/ServiceEndpointManagerBase.cs b/src/Microsoft.Azure.SignalR.Common/Endpoints/ServiceEndpointManagerBase.cs index 06ce0208a..ee95c482e 100644 --- a/src/Microsoft.Azure.SignalR.Common/Endpoints/ServiceEndpointManagerBase.cs +++ b/src/Microsoft.Azure.SignalR.Common/Endpoints/ServiceEndpointManagerBase.cs @@ -20,12 +20,11 @@ internal abstract class ServiceEndpointManagerBase : IServiceEndpointManager private readonly ILogger _logger; - // Filtered valuable endpoints from ServiceOptions - public ServiceEndpoint[] Endpoints { get; protected set; } + // Filtered valuable endpoints from ServiceOptions, use dict for fast search + public IReadOnlyDictionary Endpoints { get; private set; } public event EndpointEventHandler OnAdd; public event EndpointEventHandler OnRemove; - public event EndpointEventHandler OnRename; protected ServiceEndpointManagerBase(IServiceEndpointOptions options, ILogger logger) : this(GetEndpoints(options), logger) @@ -38,19 +37,13 @@ internal ServiceEndpointManagerBase(IEnumerable endpoints, ILog _logger = logger ?? throw new ArgumentNullException(nameof(logger)); Endpoints = GetValuableEndpoints(endpoints); - - if (Endpoints.Length > 0 && Endpoints.All(s => s.EndpointType != EndpointType.Primary)) - { - // Only throws when endpoint count > 0 - throw new AzureSignalRNoPrimaryEndpointException(); - } } public abstract IServiceEndpointProvider GetEndpointProvider(ServiceEndpoint endpoint); public IReadOnlyList GetEndpoints(string hub) { - return _endpointsPerHub.GetOrAdd(hub, s => Endpoints.Select(e => CreateHubServiceEndpoint(hub, e)).ToArray()); + return _endpointsPerHub.GetOrAdd(hub, s => Endpoints.Select(e => CreateHubServiceEndpoint(hub, e.Key)).ToArray()); } protected static IEnumerable GetEndpoints(IServiceEndpointOptions options) @@ -79,7 +72,7 @@ protected static IEnumerable GetEndpoints(IServiceEndpointOptio } } - protected ServiceEndpoint[] GetValuableEndpoints(IEnumerable endpoints) + protected Dictionary GetValuableEndpoints(IEnumerable endpoints) { // select the most valuable endpoint with the same endpoint address var groupedEndpoints = endpoints.Distinct().GroupBy(s => s.Endpoint).Select(s => @@ -94,67 +87,91 @@ protected ServiceEndpoint[] GetValuableEndpoints(IEnumerable en var item = items.FirstOrDefault(i => i.EndpointType == EndpointType.Primary) ?? items.FirstOrDefault(); Log.DuplicateEndpointFound(_logger, items.Count, item?.Endpoint, item?.ToString()); return item; - }); + }).ToDictionary(k => k, v => v, new ServiceEndpointWeakComparer()); - return groupedEndpoints.ToArray(); + if (groupedEndpoints.Count == 0) + { + throw new AzureSignalRConfigurationNoEndpointException(); + } + + if (groupedEndpoints.Count > 0 && groupedEndpoints.All(s => s.Key.EndpointType != EndpointType.Primary)) + { + // Only throws when endpoint count > 0 + throw new AzureSignalRNoPrimaryEndpointException(); + } + + return groupedEndpoints; } - protected async Task AddServiceEndpointsAsync(IReadOnlyList endpoints, CancellationToken cancellationToken) + protected async virtual Task ReloadServiceEndpointsAsync(ServiceEndpoint[] serviceEndpoints, TimeSpan scaleTimeout) { - if (endpoints.Count > 0) + try { - try - { - var hubEndpoints = CreateHubServiceEndpoints(endpoints, true); + var endpoints = GetValuableEndpoints(serviceEndpoints); - await Task.WhenAll(hubEndpoints.Select(e => AddHubServiceEndpointAsync(e, cancellationToken))); + UpdateEndpoints(endpoints, out var addedEndpoints, out var removedEndpoints); - // TODO: update local store for negotiation + using (var addCts = new CancellationTokenSource(scaleTimeout)) + { + if (!await WaitTaskOrTimeout(AddServiceEndpointsAsync(addedEndpoints, addCts.Token), addCts)) + { + Log.AddEndpointsTimeout(_logger); + } } - catch (Exception ex) + + using (var removeCts = new CancellationTokenSource(scaleTimeout)) { - Log.FailedAddingEndpoints(_logger, ex); + if (!await WaitTaskOrTimeout(RemoveServiceEndpointsAsync(removedEndpoints, removeCts.Token), removeCts)) + { + Log.RemoveEndpointsTimeout(_logger); + } } } + catch (Exception ex) + { + Log.ReloadEndpointsError(_logger, ex); + return; + } + + } - protected async Task RemoveServiceEndpointsAsync(IReadOnlyList endpoints, CancellationToken cancellationToken) + private async Task AddServiceEndpointsAsync(IEnumerable endpoints, CancellationToken cancellationToken) { - if (endpoints.Count > 0) + if (endpoints.Count() > 0) { try { var hubEndpoints = CreateHubServiceEndpoints(endpoints, true); - // TODO: update local store for negotiation + await Task.WhenAll(hubEndpoints.Select(e => AddHubServiceEndpointAsync(e, cancellationToken))); - await Task.WhenAll(hubEndpoints.Select(e => RemoveHubServiceEndpointAsync(e, cancellationToken))); + // TODO: update local store for negotiation } catch (Exception ex) { - Log.FailedRemovingEndpoints(_logger, ex); + Log.FailedAddingEndpoints(_logger, ex); } } } - protected Task RenameSerivceEndpoints(IReadOnlyList endpoints) + private async Task RemoveServiceEndpointsAsync(IEnumerable endpoints, CancellationToken cancellationToken) { - if (endpoints.Count > 0) + if (endpoints.Count() > 0) { try { - var hubEndpoints = CreateHubServiceEndpoints(endpoints, false); + var hubEndpoints = CreateHubServiceEndpoints(endpoints, true); // TODO: update local store for negotiation - return Task.WhenAll(hubEndpoints.Select(e => RenameHubServiceEndpoint(e))); + await Task.WhenAll(hubEndpoints.Select(e => RemoveHubServiceEndpointAsync(e, cancellationToken))); } catch (Exception ex) { - Log.FailedRenamingEndpoint(_logger, ex); + Log.FailedRemovingEndpoints(_logger, ex); } } - return Task.CompletedTask; } private HubServiceEndpoint CreateHubServiceEndpoint(string hub, ServiceEndpoint endpoint, bool needScaleTcs = false) @@ -198,7 +215,7 @@ private async Task RemoveHubServiceEndpointAsync(HubServiceEndpoint endpoint, Ca Log.StartRemovingEndpoint(_logger, endpoint.Endpoint, endpoint.Name); OnRemove?.Invoke(endpoint); - + // Wait for endpoint turn offline or timeout getting cancelled await Task.WhenAny(endpoint.ScaleTask, cancellationToken.AsTask()); @@ -206,12 +223,63 @@ private async Task RemoveHubServiceEndpointAsync(HubServiceEndpoint endpoint, Ca endpoint.CompleteScale(); } - private Task RenameHubServiceEndpoint(HubServiceEndpoint endpoint) + private void UpdateEndpoints(Dictionary updatedEndpoints, + out IEnumerable addedEndpoints, + out IEnumerable removedEndpoints) + { + var endpoints = new Dictionary(); + var added = new List(); + + removedEndpoints = Endpoints.Keys.Except(updatedEndpoints.Keys); + + foreach (var endpoint in updatedEndpoints) + { + // search exist from old + if (Endpoints.TryGetValue(endpoint.Key, out var value)) + { + // remained or renamed + if (value.Name != endpoint.Key.Name) + { + value.Name = endpoint.Key.Name; + } + endpoints.Add(value, value); + } + else + { + // added + endpoints.Add(endpoint.Key, endpoint.Key); + added.Add(endpoint.Key); + } + } + addedEndpoints = added; + + Endpoints = endpoints; + } + + private static async Task WaitTaskOrTimeout(Task task, CancellationTokenSource cts) { - Log.StartRenamingEndpoint(_logger, endpoint.Endpoint, endpoint.Name); + var completed = await Task.WhenAny(task, Task.Delay(Timeout.InfiniteTimeSpan, cts.Token)); + + if (completed == task) + { + return true; + } + + cts.Cancel(); + return false; + } + + private sealed class ServiceEndpointWeakComparer : IEqualityComparer + { + public bool Equals(ServiceEndpoint x, ServiceEndpoint y) + { + return x.Endpoint == y.Endpoint && x.EndpointType == y.EndpointType; + } - OnRename?.Invoke(endpoint); - return Task.CompletedTask; + public int GetHashCode(ServiceEndpoint obj) + { + return obj.Endpoint.GetHashCode() ^ obj.EndpointType.GetHashCode(); + } } private static class Log @@ -228,17 +296,20 @@ private static class Log private static readonly Action _startRenamingEndpoint = LoggerMessage.Define(LogLevel.Debug, new EventId(4, "StartRenamingEndpoint"), "Start renaming endpoint: '{endpoint}', name: '{name}'"); - private static readonly Action _failedAddingEndpoints = - LoggerMessage.Define(LogLevel.Error, new EventId(5, "FailedAddingEndpoints"), "Failed adding endpoints."); + private static readonly Action _reloadEndpointError = + LoggerMessage.Define(LogLevel.Error, new EventId(5, "ReloadEndpointsError"), "No connection string is specified. Skip scale operation."); - private static readonly Action _failedRemovingEndpoints = - LoggerMessage.Define(LogLevel.Error, new EventId(6, "FailedRemovingEndpoints"), "Failed removing endpoints."); + private static readonly Action _AddEndpointsTimeout = + LoggerMessage.Define(LogLevel.Error, new EventId(6, "AddEndpointsTimeout"), "Timeout waiting for adding endpoints."); - private static readonly Action _failedRenamingEndpoints = - LoggerMessage.Define(LogLevel.Error, new EventId(7, "StartRenamingEndpoints"), "Failed renaming endpoints."); + private static readonly Action _removeEndpointsTimeout = + LoggerMessage.Define(LogLevel.Error, new EventId(7, "RemoveEndpointsTimeout"), "Timeout waiting for removing endpoints."); - private static readonly Action _timeoutWaitingForScale = - LoggerMessage.Define(LogLevel.Error, new EventId(8, "TimeoutWaitingForScale"), "Timeout waiting '{timeout}' seconds for connection operations when scale endpoint."); + private static readonly Action _failedAddingEndpoints = + LoggerMessage.Define(LogLevel.Error, new EventId(8, "FailedAddingEndpoints"), "Failed adding endpoints."); + + private static readonly Action _failedRemovingEndpoints = + LoggerMessage.Define(LogLevel.Error, new EventId(9, "FailedRemovingEndpoints"), "Failed removing endpoints."); public static void DuplicateEndpointFound(ILogger logger, int count, string endpoint, string name) { @@ -260,24 +331,29 @@ public static void StartRenamingEndpoint(ILogger logger, string endpoint, string _startRenamingEndpoint(logger, endpoint, name, null); } - public static void FailedAddingEndpoints(ILogger logger, Exception ex) + public static void ReloadEndpointsError(ILogger logger, Exception ex) { - _failedAddingEndpoints(logger, ex); + _reloadEndpointError(logger, ex); } - public static void FailedRemovingEndpoints(ILogger logger, Exception ex) + public static void AddEndpointsTimeout(ILogger logger) { - _failedRemovingEndpoints(logger, ex); + _AddEndpointsTimeout(logger, null); } - public static void FailedRenamingEndpoint(ILogger logger, Exception ex) + public static void RemoveEndpointsTimeout(ILogger logger) { - _failedRenamingEndpoints(logger, ex); + _removeEndpointsTimeout(logger, null); } - public static void TimeoutWaitingForScale(ILogger logger, int timeout) + public static void FailedAddingEndpoints(ILogger logger, Exception ex) { - _timeoutWaitingForScale(logger, timeout, null); + _failedAddingEndpoints(logger, ex); + } + + public static void FailedRemovingEndpoints(ILogger logger, Exception ex) + { + _failedRemovingEndpoints(logger, ex); } } } diff --git a/src/Microsoft.Azure.SignalR.Common/Exceptions/AzureSignalRConfigurationNoEndpointException.cs b/src/Microsoft.Azure.SignalR.Common/Exceptions/AzureSignalRConfigurationNoEndpointException.cs new file mode 100644 index 000000000..e1fb96c8d --- /dev/null +++ b/src/Microsoft.Azure.SignalR.Common/Exceptions/AzureSignalRConfigurationNoEndpointException.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Runtime.Serialization; + +namespace Microsoft.Azure.SignalR.Common +{ + [Serializable] + public class AzureSignalRConfigurationNoEndpointException : AzureSignalRException + { + public AzureSignalRConfigurationNoEndpointException() : base("No connection string was specified.") + { + } + + protected AzureSignalRConfigurationNoEndpointException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/src/Microsoft.Azure.SignalR.Common/Interfaces/IServiceEndpointManager.cs b/src/Microsoft.Azure.SignalR.Common/Interfaces/IServiceEndpointManager.cs index 1197b4772..ce1820f86 100644 --- a/src/Microsoft.Azure.SignalR.Common/Interfaces/IServiceEndpointManager.cs +++ b/src/Microsoft.Azure.SignalR.Common/Interfaces/IServiceEndpointManager.cs @@ -11,14 +11,12 @@ internal interface IServiceEndpointManager { IServiceEndpointProvider GetEndpointProvider(ServiceEndpoint endpoint); - ServiceEndpoint[] Endpoints { get; } + IReadOnlyDictionary Endpoints { get; } IReadOnlyList GetEndpoints(string hub); event EndpointEventHandler OnAdd; event EndpointEventHandler OnRemove; - - event EndpointEventHandler OnRename; } } diff --git a/src/Microsoft.Azure.SignalR.Common/ServiceConnections/MultiEndpointServiceConnectionContainer.cs b/src/Microsoft.Azure.SignalR.Common/ServiceConnections/MultiEndpointServiceConnectionContainer.cs index 14b4057e8..c66b29ee2 100644 --- a/src/Microsoft.Azure.SignalR.Common/ServiceConnections/MultiEndpointServiceConnectionContainer.cs +++ b/src/Microsoft.Azure.SignalR.Common/ServiceConnections/MultiEndpointServiceConnectionContainer.cs @@ -18,6 +18,7 @@ internal class MultiEndpointServiceConnectionContainer : IMultiEndpointServiceCo private readonly IMessageRouter _router; private readonly ILogger _logger; private readonly IServiceEndpointManager _serviceEndpointManager; + private readonly object _lock = new object(); // private (bool needRouter, IReadOnlyList endpoints) _routerEndpoints; @@ -53,7 +54,6 @@ internal MultiEndpointServiceConnectionContainer( _serviceEndpointManager.OnAdd += OnAdd; _serviceEndpointManager.OnRemove += OnRemove; - _serviceEndpointManager.OnRename += OnRename; } public MultiEndpointServiceConnectionContainer( @@ -73,7 +73,8 @@ ILoggerFactory loggerFactory { } - public IEnumerable GetOnlineEndpoints() + // for tests + public IEnumerable GetOnlineEndpoints() { return _routerEndpoints.endpoints.Where(s => s.Online); } @@ -277,15 +278,6 @@ private Task RemoveHubServiceEndpointAsync(HubServiceEndpoint endpoint) return Task.CompletedTask; } - private void OnRename(HubServiceEndpoint endpoint) - { - if (!endpoint.Hub.Equals(_hubName, StringComparison.OrdinalIgnoreCase)) - { - return; - } - // TODO: update local store names - } - private static class Log { private static readonly Action _startingConnection = diff --git a/src/Microsoft.Azure.SignalR/EndpointProvider/ServiceEndpointManager.cs b/src/Microsoft.Azure.SignalR/EndpointProvider/ServiceEndpointManager.cs index 9ff415465..821be67a9 100644 --- a/src/Microsoft.Azure.SignalR/EndpointProvider/ServiceEndpointManager.cs +++ b/src/Microsoft.Azure.SignalR/EndpointProvider/ServiceEndpointManager.cs @@ -2,9 +2,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -20,21 +17,15 @@ internal class ServiceEndpointManager : ServiceEndpointManagerBase // Only Endpoints value accept hot-reload and prevent changes of unexpected modification on other configurations. private readonly ServiceOptions _options; private readonly TimeSpan _scaleTimeout; - private IReadOnlyList _endpointsStore; - + public ServiceEndpointManager(IOptionsMonitor optionsMonitor, ILoggerFactory loggerFactory) : base(optionsMonitor.CurrentValue, loggerFactory.CreateLogger()) { - if (Endpoints.Length == 0) - { - throw new ArgumentException(ServiceEndpointProvider.ConnectionStringNotFound); - } _options = optionsMonitor.CurrentValue; _logger = loggerFactory?.CreateLogger() ?? throw new ArgumentNullException(nameof(loggerFactory)); // TODO: Enable optionsMonitor.OnChange when feature ready. // optionsMonitor.OnChange(OnChange); - _endpointsStore = Endpoints; _scaleTimeout = _options.ServiceScaleTimeout; } @@ -48,80 +39,17 @@ public override IServiceEndpointProvider GetEndpointProvider(ServiceEndpoint end return new ServiceEndpointProvider(endpoint, _options); } - private async void OnChange(ServiceOptions options) + private void OnChange(ServiceOptions options) { Log.DetectConfigurationChanges(_logger); - // Reset local cache and validate result - var endpoints = GetValuableEndpoints(GetEndpoints(options)); - if (endpoints.Length == 0) - { - Log.EndpointNotFound(_logger); - return; - } - Endpoints = endpoints; - - var updatedEndpoints = GetChangedEndpoints(Endpoints); - - await RenameSerivceEndpoints(updatedEndpoints.RenamedEndpoints); - - using (var addCts = new CancellationTokenSource(options.ServiceScaleTimeout)) - { - if (!await WaitTaskOrTimeout(AddServiceEndpointsAsync(updatedEndpoints.AddedEndpoints, addCts.Token), addCts)) - { - Log.TimeoutAddEndpoints(_logger); - } - } - - using (var removeCts = new CancellationTokenSource(options.ServiceScaleTimeout)) - { - if (!await WaitTaskOrTimeout(RemoveServiceEndpointsAsync(updatedEndpoints.RemovedEndpoints, removeCts.Token), removeCts)) - { - Log.TimeoutRemoveEndpoints(_logger); - } - } - - _endpointsStore = Endpoints; - } - - private (IReadOnlyList AddedEndpoints, - IReadOnlyList RemovedEndpoints, - IReadOnlyList RenamedEndpoints) - GetChangedEndpoints(IEnumerable updatedEndpoints) - { - var originalEndpoints = _endpointsStore; - var addedEndpoints = updatedEndpoints.Except(originalEndpoints, new ServiceEndpointWeakComparer()).ToList(); - var removedEndpoints = originalEndpoints.Except(updatedEndpoints, new ServiceEndpointWeakComparer()).ToList(); - - var renamedEndpoints = updatedEndpoints.Except(originalEndpoints).Except(addedEndpoints).ToList(); - - return (AddedEndpoints: addedEndpoints, RemovedEndpoints: removedEndpoints, RenamedEndpoints: renamedEndpoints); - } - - private static async Task WaitTaskOrTimeout(Task task, CancellationTokenSource cts) - { - var completed = await Task.WhenAny(task, Task.Delay(Timeout.InfiniteTimeSpan, cts.Token)); - - if (completed == task) - { - return true; - } - - cts.Cancel(); - return false; + ReloadServiceEndpointsAsync(options.Endpoints); } - private sealed class ServiceEndpointWeakComparer : IEqualityComparer + // TODO: make public for non hot-reload plans + private Task ReloadServiceEndpointsAsync(ServiceEndpoint[] serviceEndpoints) { - public bool Equals(ServiceEndpoint x, ServiceEndpoint y) - { - return x.Endpoint == y.Endpoint && x.EndpointType == y.EndpointType; - } - - public int GetHashCode(ServiceEndpoint obj) - { - return obj.Endpoint.GetHashCode() ^ obj.EndpointType.GetHashCode(); - } + return ReloadServiceEndpointsAsync(serviceEndpoints, _scaleTimeout); } private static class Log @@ -129,42 +57,10 @@ private static class Log private static readonly Action _detectEndpointChanges = LoggerMessage.Define(LogLevel.Debug, new EventId(1, "DetectConfigurationChanges"), "Dected configuration changes in configuration, start live-scale."); - private static readonly Action _endpointNotFound = - LoggerMessage.Define(LogLevel.Warning, new EventId(2, "EndpointNotFound"), "No connection string is specified. Skip scale operation."); - - private static readonly Action _timeoutRenameEndpoints = - LoggerMessage.Define(LogLevel.Error, new EventId(3, "TimeoutRenameEndpoints"), "Timeout waiting for renaming endpoints."); - - private static readonly Action _timeoutAddEndpoints = - LoggerMessage.Define(LogLevel.Error, new EventId(4, "TimeoutAddEndpoints"), "Timeout waiting for adding endpoints."); - - private static readonly Action _timeoutRemoveEndpoints = - LoggerMessage.Define(LogLevel.Error, new EventId(5, "TimeoutRemoveEndpoints"), "Timeout waiting for removing endpoints."); - public static void DetectConfigurationChanges(ILogger logger) { _detectEndpointChanges(logger, null); } - - public static void EndpointNotFound(ILogger logger) - { - _endpointNotFound(logger, null); - } - - public static void TimeoutRenameEndpoints(ILogger logger) - { - _timeoutRenameEndpoints(logger, null); - } - - public static void TimeoutAddEndpoints(ILogger logger) - { - _timeoutAddEndpoints(logger, null); - } - - public static void TimeoutRemoveEndpoints(ILogger logger) - { - _timeoutRemoveEndpoints(logger, null); - } } } } diff --git a/test/Microsoft.Azure.SignalR.AspNet.Tests/MultiEndpointServiceConnectionContainerTests.cs b/test/Microsoft.Azure.SignalR.AspNet.Tests/MultiEndpointServiceConnectionContainerTests.cs index 72f2eb86f..3c745eb65 100644 --- a/test/Microsoft.Azure.SignalR.AspNet.Tests/MultiEndpointServiceConnectionContainerTests.cs +++ b/test/Microsoft.Azure.SignalR.AspNet.Tests/MultiEndpointServiceConnectionContainerTests.cs @@ -155,7 +155,7 @@ public async Task TestEndpointManagerWithDuplicateEndpoints() new ServiceEndpoint(ConnectionString2, EndpointType.Secondary, "11"), new ServiceEndpoint(ConnectionString2, EndpointType.Secondary, "12") ); - var endpoints = sem.Endpoints; + var endpoints = sem.Endpoints.Keys.OrderBy(x => x.Name).ToArray(); Assert.Equal(2, endpoints.Length); Assert.Equal("1", endpoints[0].Name); Assert.Equal("11", endpoints[1].Name); @@ -183,7 +183,7 @@ public async Task TestEndpointManagerWithDuplicateEndpointsAndConnectionStarted( new ServiceEndpoint(ConnectionString2, EndpointType.Secondary, "11"), new ServiceEndpoint(ConnectionString2, EndpointType.Secondary, "12") ); - var endpoints = sem.Endpoints.ToArray(); + var endpoints = sem.Endpoints.Keys.OrderBy(x => x.Name).ToArray(); Assert.Equal(2, endpoints.Length); Assert.Equal("1", endpoints[0].Name); Assert.Equal("11", endpoints[1].Name); @@ -205,11 +205,9 @@ public async Task TestEndpointManagerWithDuplicateEndpointsAndConnectionStarted( } [Fact] - public void TestContainerWithNoEndpointDontThrowFromBaseClass() + public void TestContainerWithNoEndpointThrowException() { - var manager = new TestServiceEndpointManager(); - var endpoints = manager.Endpoints; - Assert.Empty(endpoints); + Assert.Throws(() => new TestServiceEndpointManager()); } [Fact] diff --git a/test/Microsoft.Azure.SignalR.AspNet.Tests/RunAzureSignalRTests.cs b/test/Microsoft.Azure.SignalR.AspNet.Tests/RunAzureSignalRTests.cs index 3afd895d0..db4a89c94 100644 --- a/test/Microsoft.Azure.SignalR.AspNet.Tests/RunAzureSignalRTests.cs +++ b/test/Microsoft.Azure.SignalR.AspNet.Tests/RunAzureSignalRTests.cs @@ -14,6 +14,7 @@ using Microsoft.AspNet.SignalR.Hubs; using Microsoft.AspNet.SignalR.Messaging; using Microsoft.AspNet.SignalR.Transports; +using Microsoft.Azure.SignalR.Common; using Microsoft.Azure.SignalR.Protocol; using Microsoft.Azure.SignalR.Tests.Common; using Microsoft.Extensions.DependencyInjection; @@ -80,7 +81,7 @@ public void TestRunAzureSignalRWithAppNameEqualToHubNameThrows() [Fact] public void TestRunAzureSignalRWithoutConnectionString() { - var exception = Assert.Throws( + var exception = Assert.Throws( () => { using (WebApp.Start(ServiceUrl, app => app.RunAzureSignalR(AppName))) @@ -167,7 +168,7 @@ public void TestRunAzureSignalRWithMultipleAppSettings() var manager = hubConfig.Resolver.Resolve(); var endpoints = manager.Endpoints; - Assert.Equal(4, endpoints.Length); + Assert.Equal(4, endpoints.Count); } } } @@ -190,9 +191,9 @@ public void TestRunAzureSignalRWithSingleAppSettingsAndConfigureOptions() Assert.Empty(options.Value.Endpoints); var manager = hubConfig.Resolver.Resolve(); - var endpoints = manager.Endpoints; + var endpoints = manager.Endpoints.Keys; Assert.Single(endpoints); - Assert.Equal(ConnectionString2, endpoints[0].ConnectionString); + Assert.Equal(ConnectionString2, endpoints.First().ConnectionString); } } } @@ -216,7 +217,7 @@ public void TestRunAzureSignalRWithMultipleAppSettingsAndConnectionStringConfigu var manager = hubConfig.Resolver.Resolve(); var endpoints = manager.Endpoints; - Assert.Equal(3, endpoints.Length); + Assert.Equal(3, endpoints.Count); } } } @@ -245,7 +246,7 @@ public void TestRunAzureSignalRWithMultipleAppSettingsAndEndpointsConfigured() var manager = hubConfig.Resolver.Resolve(); var endpoints = manager.Endpoints; - Assert.Equal(4, endpoints.Length); + Assert.Equal(4, endpoints.Count); } } } @@ -282,7 +283,7 @@ public void TestRunAzureSignalRWithMultipleAppSettingsAndBothConnectionStringAnd var manager = hubConfig.Resolver.Resolve(); var endpoints = manager.Endpoints; - Assert.Equal(expectedCount, endpoints.Length); + Assert.Equal(expectedCount, endpoints.Count); } } } @@ -316,7 +317,7 @@ public void TestRunAzureSignalRWithMultipleAppSettingsAndBothConnectionStringAnd var manager = hubConfig.Resolver.Resolve(); var endpoints = manager.Endpoints; - Assert.Equal(4, endpoints.Length); + Assert.Equal(4, endpoints.Count); } } } @@ -347,7 +348,7 @@ public void TestRunAzureSignalRWithMultipleAppSettingsAndCustomSettings() var manager = hubConfig.Resolver.Resolve(); var endpoints = manager.Endpoints; - Assert.Equal(4, endpoints.Length); + Assert.Equal(4, endpoints.Count); } } } @@ -379,7 +380,7 @@ public void TestRunAzureSignalRWithMultipleAppSettingsAndCustomSettingsIncluding var manager = hubConfig.Resolver.Resolve(); var endpoints = manager.Endpoints; - Assert.Equal(4, endpoints.Length); + Assert.Equal(4, endpoints.Count); } } } @@ -412,7 +413,7 @@ public async Task TestRunAzureSignalRWithMultipleAppSettingsAndCustomSettingsAnd var manager = hubConfig.Resolver.Resolve(); var endpoints = manager.Endpoints; - Assert.Equal(4, endpoints.Length); + Assert.Equal(4, endpoints.Count); var client = new HttpClient { BaseAddress = new Uri(ServiceUrl) }; var response = await client.GetAsync("/negotiate?endpoint=chosen"); diff --git a/test/Microsoft.Azure.SignalR.Tests/AddAzureSignalRFacts.cs b/test/Microsoft.Azure.SignalR.Tests/AddAzureSignalRFacts.cs index 869f9c048..b9a54768c 100644 --- a/test/Microsoft.Azure.SignalR.Tests/AddAzureSignalRFacts.cs +++ b/test/Microsoft.Azure.SignalR.Tests/AddAzureSignalRFacts.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading.Tasks; using Microsoft.Azure.SignalR.Common; using Microsoft.Azure.SignalR.Tests.Common; @@ -235,7 +236,7 @@ public void AddAzureSignalRReadServiceEndpointsFromConfig(string customValue, st } // Endpoints from Endpoints and ConnectionString config are merged inside the EndpointManager - var endpoints = serviceProvider.GetRequiredService().Endpoints; + var endpoints = serviceProvider.GetRequiredService().Endpoints.Keys.ToArray(); if (secondaryValue == null) { // When no other connection string is defined, endpoints value is always connection string value @@ -352,9 +353,9 @@ public void AddAzureSignalRCustomizeEndpointsOverridesConfigValue(string default var endpointManager = serviceProvider.GetRequiredService(); - var endpoints = endpointManager.Endpoints; + var endpoints = endpointManager.Endpoints.Keys; - Assert.Equal(expectedCount, endpoints.Length); + Assert.Equal(expectedCount, endpoints.Count()); Assert.Contains(endpoints, s => s.ConnectionString == CustomValue && s.EndpointType == EndpointType.Primary); diff --git a/test/Microsoft.Azure.SignalR.Tests/AzureSignalRMarkerServiceFact.cs b/test/Microsoft.Azure.SignalR.Tests/AzureSignalRMarkerServiceFact.cs index 60ba8934c..f673c04c1 100644 --- a/test/Microsoft.Azure.SignalR.Tests/AzureSignalRMarkerServiceFact.cs +++ b/test/Microsoft.Azure.SignalR.Tests/AzureSignalRMarkerServiceFact.cs @@ -7,6 +7,7 @@ using System.Threading; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.SignalR; +using Microsoft.Azure.SignalR.Common; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -139,7 +140,7 @@ public void UseAzureSignalRWithConnectionStringNotSpecified() .BuildServiceProvider(); var app = new ApplicationBuilder(serviceProvider); - var exception = Assert.Throws(() => app.UseAzureSignalR(routes => + var exception = Assert.Throws(() => app.UseAzureSignalR(routes => { routes.MapHub("/chat"); })); diff --git a/test/Microsoft.Azure.SignalR.Tests/Infrastructure/ServiceConnectionProxy.cs b/test/Microsoft.Azure.SignalR.Tests/Infrastructure/ServiceConnectionProxy.cs index fb5af6065..25611035a 100644 --- a/test/Microsoft.Azure.SignalR.Tests/Infrastructure/ServiceConnectionProxy.cs +++ b/test/Microsoft.Azure.SignalR.Tests/Infrastructure/ServiceConnectionProxy.cs @@ -59,7 +59,7 @@ public ServiceConnectionProxy( ServerNameProvider = new DefaultServerNameProvider(); // these two lines should be located in the end of this constructor. - ServiceConnectionContainer = new StrongServiceConnectionContainer(this, connectionCount, new HubServiceEndpoint("", null, null), NullLogger.Instance); + ServiceConnectionContainer = new StrongServiceConnectionContainer(this, connectionCount, new HubServiceEndpoint(), NullLogger.Instance); ServiceMessageHandler = (StrongServiceConnectionContainer) ServiceConnectionContainer; } diff --git a/test/Microsoft.Azure.SignalR.Tests/MultiEndpointServiceConnectionContainerTests.cs b/test/Microsoft.Azure.SignalR.Tests/MultiEndpointServiceConnectionContainerTests.cs index 9aeacee40..ee9a0d205 100644 --- a/test/Microsoft.Azure.SignalR.Tests/MultiEndpointServiceConnectionContainerTests.cs +++ b/test/Microsoft.Azure.SignalR.Tests/MultiEndpointServiceConnectionContainerTests.cs @@ -23,8 +23,10 @@ public class TestEndpointServiceConnectionContainerTests : VerifiableLoggedTest private const string ConnectionStringFormatter = "Endpoint={0};AccessKey=ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789;"; private const string Url1 = "http://url1"; private const string Url2 = "https://url2"; + private const string Url3 = "http://url3"; private readonly string ConnectionString1 = string.Format(ConnectionStringFormatter, Url1); private readonly string ConnectionString2 = string.Format(ConnectionStringFormatter, Url2); + private readonly string ConnectionString3 = string.Format(ConnectionStringFormatter, Url3); private static readonly JoinGroupWithAckMessage DefaultGroupMessage = new JoinGroupWithAckMessage("a", "a", -1); public TestEndpointServiceConnectionContainerTests(ITestOutputHelper output) : base(output) @@ -69,7 +71,7 @@ public void TestEndpointManagerWithDuplicateEndpoints() new ServiceEndpoint(ConnectionString2, EndpointType.Secondary, "11"), new ServiceEndpoint(ConnectionString2, EndpointType.Secondary, "12") ); - var endpoints = sem.Endpoints; + var endpoints = sem.Endpoints.Keys.OrderBy(x => x.Name).ToArray(); Assert.Equal(2, endpoints.Length); Assert.Equal("1", endpoints[0].Name); Assert.Equal("11", endpoints[1].Name); @@ -94,7 +96,7 @@ public async Task TestEndpointManagerWithDuplicateEndpointsAndConnectionStarted( new ServiceEndpoint(ConnectionString2, EndpointType.Secondary, "11"), new ServiceEndpoint(ConnectionString2, EndpointType.Secondary, "12") ); - var endpoints = sem.Endpoints; + var endpoints = sem.Endpoints.Keys.OrderBy(x => x.Name).ToArray(); Assert.Equal(2, endpoints.Length); Assert.Equal("1", endpoints[0].Name); Assert.Equal("11", endpoints[1].Name); @@ -117,11 +119,9 @@ public async Task TestEndpointManagerWithDuplicateEndpointsAndConnectionStarted( } [Fact] - public void TestContainerWithNoEndpointDontThrowFromBaseClass() + public void TestContainerWithNoEndpointThrowNoEndpointException() { - var manager = new TestServiceEndpointManager(); - var endpoints = manager.Endpoints; - Assert.Empty(endpoints); + Assert.Throws(() => new TestServiceEndpointManager()); } [Fact] @@ -746,6 +746,159 @@ public async Task TestMultiEndpointOffline(bool migratable) await TestEndpointOfflineInner(manager, new TestEndpointRouter(), migratable); } + [Fact] + public async Task TestMultipleEndpointWithRenamesAndWriteAckableMessage() + { + var sem = new TestServiceEndpointManager( + new ServiceEndpoint(ConnectionString1, EndpointType.Primary, "1"), + new ServiceEndpoint(ConnectionString2, EndpointType.Primary, "2"), + new ServiceEndpoint(ConnectionString3, EndpointType.Secondary, "3") + ); + + var writeTcs = new TaskCompletionSource(); + var endpoints = sem.Endpoints.Keys.OrderBy(x => x.Name).ToArray(); + Assert.Equal(3, endpoints.Length); + Assert.Equal("1", endpoints[0].Name); + Assert.Equal("2", endpoints[1].Name); + Assert.Equal("3", endpoints[2].Name); + + var router = new TestEndpointRouter(); + var containers = new Dictionary(); + var container = new TestMultiEndpointServiceConnectionContainer("hub", + e => containers[e] = new TestServiceConnectionContainer(new List { + new TestSimpleServiceConnection(writeAsyncTcs: writeTcs), + new TestSimpleServiceConnection(writeAsyncTcs: writeTcs), + new TestSimpleServiceConnection(writeAsyncTcs: writeTcs) + }, e), sem, router, NullLoggerFactory.Instance); + + // All the connections started + _ = container.StartAsync(); + await container.ConnectionInitializedTask; + + var containerEps = container.GetOnlineEndpoints().OrderBy(x => x.Name).ToArray(); + var container1 = containerEps[0].ConnectionContainer; + Assert.Equal(3, containerEps.Length); + Assert.Equal("1", containerEps[0].Name); + Assert.Equal("2", containerEps[1].Name); + Assert.Equal("3", containerEps[2].Name); + + // Trigger reload to test rename + var renamedEndpoint = new ServiceEndpoint[] + { + new ServiceEndpoint(ConnectionString1, EndpointType.Primary, "11"), + new ServiceEndpoint(ConnectionString2, EndpointType.Primary, "2"), + new ServiceEndpoint(ConnectionString3, EndpointType.Secondary, "33") + }; + await sem.TestReloadServiceEndpoints(renamedEndpoint); + + // validate container level updates + containerEps = container.GetOnlineEndpoints().OrderBy(x => x.Name).ToArray(); + Assert.Equal(3, containerEps.Length); + Assert.Equal("11", containerEps[0].Name); + // container1 keep same after rename + Assert.Equal(container1, containerEps[0].ConnectionContainer); + Assert.Equal("2", containerEps[1].Name); + Assert.Equal("33", containerEps[2].Name); + + // validate sem negotiation endpoints updated + var ngoEps = sem.GetEndpoints("hub").OrderBy(x => x.Name).ToArray(); + Assert.Equal(3, ngoEps.Length); + Assert.Equal("11", ngoEps[0].Name); + Assert.Equal("2", ngoEps[1].Name); + Assert.Equal("33", ngoEps[2].Name); + + // write messages + var task = container.WriteAckableMessageAsync(DefaultGroupMessage); + await writeTcs.Task.OrTimeout(); + containers.First().Value.HandleAck(new AckMessage(1, (int)AckStatus.Ok)); + await task.OrTimeout(); + } + + [Theory] + [MemberData(nameof(TestReloadEndpointsData))] + public void TestServiceEndpointManagerReloadEndpoints(ServiceEndpoint[] oldValue, ServiceEndpoint[] newValue) + { + var sem = new TestServiceEndpointManager(oldValue); + + sem.TestReloadServiceEndpoints(newValue); + + var endpoints = sem.Endpoints.Keys; + + Assert.True(newValue.SequenceEqual(endpoints)); + } + + public static IEnumerable TestReloadEndpointsData = new object[][] + { + // no change + new object[] + { + new ServiceEndpoint[] + { + new ServiceEndpoint("Endpoint=http://url1;AccessKey=ABCDEFG", EndpointType.Primary, "1"), + new ServiceEndpoint("Endpoint=http://url2;AccessKey=ABCDEFG", EndpointType.Primary, "2") + }, + new ServiceEndpoint[] + { + new ServiceEndpoint("Endpoint=http://url1;AccessKey=ABCDEFG", EndpointType.Primary, "1"), + new ServiceEndpoint("Endpoint=http://url2;AccessKey=ABCDEFG", EndpointType.Primary, "2") + } + }, + // add + new object[] + { + new ServiceEndpoint[] + { + new ServiceEndpoint("Endpoint=http://url1;AccessKey=ABCDEFG", EndpointType.Primary, "1") + }, + new ServiceEndpoint[] + { + new ServiceEndpoint("Endpoint=http://url1;AccessKey=ABCDEFG", EndpointType.Primary, "1"), + new ServiceEndpoint("Endpoint=http://url2;AccessKey=ABCDEFG", EndpointType.Primary, "2") + } + }, + // remove + new object[] + { + new ServiceEndpoint[] + { + new ServiceEndpoint("Endpoint=http://url1;AccessKey=ABCDEFG", EndpointType.Primary, "1"), + new ServiceEndpoint("Endpoint=http://url2;AccessKey=ABCDEFG", EndpointType.Primary, "2") + }, + new ServiceEndpoint[] + { + new ServiceEndpoint("Endpoint=http://url1;AccessKey=ABCDEFG", EndpointType.Primary, "1") + } + }, + // rename + new object[] + { + new ServiceEndpoint[] + { + new ServiceEndpoint("Endpoint=http://url1;AccessKey=ABCDEFG", EndpointType.Primary, "1"), + new ServiceEndpoint("Endpoint=http://url2;AccessKey=ABCDEFG", EndpointType.Primary, "2") + }, + new ServiceEndpoint[] + { + new ServiceEndpoint("Endpoint=http://url1;AccessKey=ABCDEFG", EndpointType.Primary, "22"), + new ServiceEndpoint("Endpoint=http://url2;AccessKey=ABCDEFG", EndpointType.Primary, "11") + } + }, + // type + new object[] + { + new ServiceEndpoint[] + { + new ServiceEndpoint("Endpoint=http://url1;AccessKey=ABCDEFG", EndpointType.Primary, "1"), + new ServiceEndpoint("Endpoint=http://url2;AccessKey=ABCDEFG", EndpointType.Secondary, "2") + }, + new ServiceEndpoint[] + { + new ServiceEndpoint("Endpoint=http://url1;AccessKey=ABCDEFG", EndpointType.Secondary, "1"), + new ServiceEndpoint("Endpoint=http://url2;AccessKey=ABCDEFG", EndpointType.Primary, "2") + } + } + }; + private async Task TestEndpointOfflineInner(IServiceEndpointManager manager, IEndpointRouter router, bool migratable) { var containers = new List(); @@ -820,6 +973,11 @@ public override IServiceEndpointProvider GetEndpointProvider(ServiceEndpoint end { return null; } + + public Task TestReloadServiceEndpoints(ServiceEndpoint[] serviceEndpoints, int timeoutSec = 0) + { + return ReloadServiceEndpointsAsync(serviceEndpoints, TimeSpan.FromSeconds(timeoutSec)); + } } private class TestEndpointRouter : EndpointRouterDecorator From 0464c3f8e335974b4a4e42cb1ed2f15cd88fe454 Mon Sep 17 00:00:00 2001 From: JialinXin Date: Mon, 16 Mar 2020 14:07:39 +0800 Subject: [PATCH 12/14] support ServiceOption to specify SecurityAlgorithm (#845) --- .../ServiceEndpointProvider.cs | 6 ++-- .../Middleware/NegotiateMiddleware.cs | 2 ++ .../ServiceOptions.cs | 8 ++++- .../AccessTokenAlgorithm.cs | 11 +++++++ .../Utilities/AuthenticationHelper.cs | 27 +++++++++++++---- .../RestApiAccessTokenGenerator.cs | 2 +- .../ServiceEndpointProvider.cs | 6 ++-- src/Microsoft.Azure.SignalR/ServiceOptions.cs | 11 +++++-- .../ServiceEndpointProviderTests.cs | 30 +++++++++++++++++++ .../AuthenticationHelperTest.cs | 2 +- .../ServiceEndpointProviderFacts.cs | 26 ++++++++++++++++ 11 files changed, 116 insertions(+), 15 deletions(-) create mode 100644 src/Microsoft.Azure.SignalR.Common/AccessTokenAlgorithm.cs diff --git a/src/Microsoft.Azure.SignalR.AspNet/EndpointProvider/ServiceEndpointProvider.cs b/src/Microsoft.Azure.SignalR.AspNet/EndpointProvider/ServiceEndpointProvider.cs index ad5c42493..278d6d649 100644 --- a/src/Microsoft.Azure.SignalR.AspNet/EndpointProvider/ServiceEndpointProvider.cs +++ b/src/Microsoft.Azure.SignalR.AspNet/EndpointProvider/ServiceEndpointProvider.cs @@ -24,6 +24,7 @@ internal class ServiceEndpointProvider : IServiceEndpointProvider private readonly string _appName; private readonly int? _port; private readonly TimeSpan _accessTokenLifetime; + private readonly AccessTokenAlgorithm _algorithm; public IWebProxy Proxy { get; } @@ -42,6 +43,7 @@ public ServiceEndpointProvider(ServiceEndpoint endpoint, ServiceOptions options) _accessKey = endpoint.AccessKey; _appName = options.ApplicationName; _port = endpoint.Port; + _algorithm = options.AccessTokenAlgorithm; Proxy = options.Proxy; } @@ -54,7 +56,7 @@ private string GetPrefixedHubName(string applicationName, string hubName) public string GenerateClientAccessToken(string hubName = null, IEnumerable claims = null, TimeSpan? lifetime = null) { var audience = $"{_endpoint}/{ClientPath}"; - return AuthenticationHelper.GenerateAccessToken(_accessKey, audience, claims, lifetime ?? _accessTokenLifetime); + return AuthenticationHelper.GenerateAccessToken(_accessKey, audience, claims, lifetime ?? _accessTokenLifetime, _algorithm); } public string GenerateServerAccessToken(string hubName, string userId, TimeSpan? lifetime = null) @@ -70,7 +72,7 @@ public string GenerateServerAccessToken(string hubName, string userId, TimeSpan? var audience = $"{_endpoint}/{ServerPath}/?hub={GetPrefixedHubName(_appName, hubName)}"; - return AuthenticationHelper.GenerateAccessToken(_accessKey, audience, claims, lifetime ?? _accessTokenLifetime); + return AuthenticationHelper.GenerateAccessToken(_accessKey, audience, claims, lifetime ?? _accessTokenLifetime, _algorithm); } public string GetClientEndpoint(string hubName = null, string originalPath = null, string queryString = null) diff --git a/src/Microsoft.Azure.SignalR.AspNet/Middleware/NegotiateMiddleware.cs b/src/Microsoft.Azure.SignalR.AspNet/Middleware/NegotiateMiddleware.cs index 49b7dc8e5..673fa36e5 100644 --- a/src/Microsoft.Azure.SignalR.AspNet/Middleware/NegotiateMiddleware.cs +++ b/src/Microsoft.Azure.SignalR.AspNet/Middleware/NegotiateMiddleware.cs @@ -43,6 +43,7 @@ internal class NegotiateMiddleware : OwinMiddleware private readonly ServerStickyMode _mode; private readonly bool _enableDetailedErrors; private readonly int _endpointsCount; + private readonly AccessTokenAlgorithm _authAlgorithm; public NegotiateMiddleware(OwinMiddleware next, HubConfiguration configuration, string appName, IServiceEndpointManager endpointManager, IEndpointRouter router, ServiceOptions options, IServerNameProvider serverNameProvider, IConnectionRequestIdProvider connectionRequestIdProvider, ILoggerFactory loggerFactory) : base(next) @@ -59,6 +60,7 @@ public NegotiateMiddleware(OwinMiddleware next, HubConfiguration configuration, _mode = options.ServerStickyMode; _enableDetailedErrors = configuration.EnableDetailedErrors; _endpointsCount = options.Endpoints.Length; + _authAlgorithm = options.AccessTokenAlgorithm; } public override Task Invoke(IOwinContext owinContext) diff --git a/src/Microsoft.Azure.SignalR.AspNet/ServiceOptions.cs b/src/Microsoft.Azure.SignalR.AspNet/ServiceOptions.cs index dbeafff7f..77b554bcb 100644 --- a/src/Microsoft.Azure.SignalR.AspNet/ServiceOptions.cs +++ b/src/Microsoft.Azure.SignalR.AspNet/ServiceOptions.cs @@ -4,9 +4,9 @@ using System; using System.Collections.Generic; using System.Configuration; -using System.Linq; using System.Net; using System.Security.Claims; +using Microsoft.IdentityModel.Tokens; using Microsoft.Owin; namespace Microsoft.Azure.SignalR.AspNet @@ -44,6 +44,12 @@ public class ServiceOptions : IServiceEndpointOptions /// public TimeSpan AccessTokenLifetime { get; set; } = Constants.DefaultAccessTokenLifetime; + /// + /// Gets or sets the access token generate algorithm, supports or + /// Default value is + /// + public AccessTokenAlgorithm AccessTokenAlgorithm { get; set; } = AccessTokenAlgorithm.HS256; + /// /// Customize the multiple endpoints used /// diff --git a/src/Microsoft.Azure.SignalR.Common/AccessTokenAlgorithm.cs b/src/Microsoft.Azure.SignalR.Common/AccessTokenAlgorithm.cs new file mode 100644 index 000000000..dbebba824 --- /dev/null +++ b/src/Microsoft.Azure.SignalR.Common/AccessTokenAlgorithm.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Azure.SignalR +{ + public enum AccessTokenAlgorithm + { + HS256, + HS512 + } +} diff --git a/src/Microsoft.Azure.SignalR.Common/Utilities/AuthenticationHelper.cs b/src/Microsoft.Azure.SignalR.Common/Utilities/AuthenticationHelper.cs index b74a83b4b..e7b2159e8 100644 --- a/src/Microsoft.Azure.SignalR.Common/Utilities/AuthenticationHelper.cs +++ b/src/Microsoft.Azure.SignalR.Common/Utilities/AuthenticationHelper.cs @@ -26,13 +26,19 @@ public static string GenerateJwtBearer( DateTime? expires = null, string signingKey = null, DateTime? issuedAt = null, - DateTime? notBefore = null) + DateTime? notBefore = null, + AccessTokenAlgorithm algorithm = AccessTokenAlgorithm.HS256) { var subject = claims == null ? null : new ClaimsIdentity(claims); - return GenerateJwtBearer(issuer, audience, subject, expires, signingKey, issuedAt, notBefore); + return GenerateJwtBearer(issuer, audience, subject, expires, signingKey, issuedAt, notBefore, algorithm); } - public static string GenerateAccessToken(string signingKey, string audience, IEnumerable claims, TimeSpan lifetime) + public static string GenerateAccessToken( + string signingKey, + string audience, + IEnumerable claims, + TimeSpan lifetime, + AccessTokenAlgorithm algorithm) { var expire = DateTime.UtcNow.Add(lifetime); @@ -40,7 +46,8 @@ public static string GenerateAccessToken(string signingKey, string audience, IEn audience: audience, claims: claims, expires: expire, - signingKey: signingKey + signingKey: signingKey, + algorithm: algorithm ); if (jwtToken.Length > MaxTokenLength) @@ -63,7 +70,8 @@ private static string GenerateJwtBearer( DateTime? expires = null, string signingKey = null, DateTime? issuedAt = null, - DateTime? notBefore = null) + DateTime? notBefore = null, + AccessTokenAlgorithm algorithm = AccessTokenAlgorithm.HS256) { SigningCredentials credentials = null; if (!string.IsNullOrEmpty(signingKey)) @@ -72,7 +80,7 @@ private static string GenerateJwtBearer( // From version 5.5.0, SignatureProvider caching is turned On by default, assign KeyId to enable correct cache for same SigningKey var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(signingKey)); securityKey.KeyId = signingKey; - credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256); + credentials = new SigningCredentials(securityKey, GetSecurityAlgorithm(algorithm)); } var token = JwtTokenHandler.CreateJwtSecurityToken( @@ -85,5 +93,12 @@ private static string GenerateJwtBearer( signingCredentials: credentials); return JwtTokenHandler.WriteToken(token); } + + private static string GetSecurityAlgorithm(AccessTokenAlgorithm algorithm) + { + return algorithm == AccessTokenAlgorithm.HS256 ? + SecurityAlgorithms.HmacSha256 : + SecurityAlgorithms.HmacSha512; + } } } diff --git a/src/Microsoft.Azure.SignalR.Management/RestApiAccessTokenGenerator.cs b/src/Microsoft.Azure.SignalR.Management/RestApiAccessTokenGenerator.cs index a423b5dc8..918f6bfd7 100644 --- a/src/Microsoft.Azure.SignalR.Management/RestApiAccessTokenGenerator.cs +++ b/src/Microsoft.Azure.SignalR.Management/RestApiAccessTokenGenerator.cs @@ -22,7 +22,7 @@ public RestApiAccessTokenGenerator(string accessKey) public string Generate(string audience, TimeSpan? lifetime = null) { - return AuthenticationHelper.GenerateAccessToken(_accessKey, audience, _claims, lifetime ?? Constants.DefaultAccessTokenLifetime); + return AuthenticationHelper.GenerateAccessToken(_accessKey, audience, _claims, lifetime ?? Constants.DefaultAccessTokenLifetime, AccessTokenAlgorithm.HS256); } private static string GenerateServerName() diff --git a/src/Microsoft.Azure.SignalR/EndpointProvider/ServiceEndpointProvider.cs b/src/Microsoft.Azure.SignalR/EndpointProvider/ServiceEndpointProvider.cs index cc4165e62..aad887341 100644 --- a/src/Microsoft.Azure.SignalR/EndpointProvider/ServiceEndpointProvider.cs +++ b/src/Microsoft.Azure.SignalR/EndpointProvider/ServiceEndpointProvider.cs @@ -19,6 +19,7 @@ internal class ServiceEndpointProvider : IServiceEndpointProvider private readonly string _appName; private readonly TimeSpan _accessTokenLifetime; private readonly IServiceEndpointGenerator _generator; + private readonly AccessTokenAlgorithm _algorithm; public IWebProxy Proxy { get; } @@ -33,6 +34,7 @@ public ServiceEndpointProvider(ServiceEndpoint endpoint, ServiceOptions serviceO _accessTokenLifetime = serviceOptions.AccessTokenLifetime; _accessKey = endpoint.AccessKey; _appName = serviceOptions.ApplicationName; + _algorithm = serviceOptions.AccessTokenAlgorithm; Proxy = serviceOptions.Proxy; var port = endpoint.Port; @@ -50,7 +52,7 @@ public string GenerateClientAccessToken(string hubName, IEnumerable claim var audience = _generator.GetClientAudience(hubName, _appName); - return AuthenticationHelper.GenerateAccessToken(_accessKey, audience, claims, lifetime ?? _accessTokenLifetime); + return AuthenticationHelper.GenerateAccessToken(_accessKey, audience, claims, lifetime ?? _accessTokenLifetime, _algorithm); } public string GenerateServerAccessToken(string hubName, string userId, TimeSpan? lifetime = null) @@ -63,7 +65,7 @@ public string GenerateServerAccessToken(string hubName, string userId, TimeSpan? var audience = _generator.GetServerAudience(hubName, _appName); var claims = userId != null ? new[] { new Claim(ClaimTypes.NameIdentifier, userId) } : null; - return AuthenticationHelper.GenerateAccessToken(_accessKey, audience, claims, lifetime ?? _accessTokenLifetime); + return AuthenticationHelper.GenerateAccessToken(_accessKey, audience, claims, lifetime ?? _accessTokenLifetime, _algorithm); } public string GetClientEndpoint(string hubName, string originalPath, string queryString) diff --git a/src/Microsoft.Azure.SignalR/ServiceOptions.cs b/src/Microsoft.Azure.SignalR/ServiceOptions.cs index c789a2a0b..b2478b0f7 100644 --- a/src/Microsoft.Azure.SignalR/ServiceOptions.cs +++ b/src/Microsoft.Azure.SignalR/ServiceOptions.cs @@ -6,7 +6,8 @@ using System.Net; using System.Security.Claims; using Microsoft.AspNetCore.Http; - +using Microsoft.IdentityModel.Tokens; + namespace Microsoft.Azure.SignalR { /// @@ -39,7 +40,13 @@ public class ServiceOptions : IServiceEndpointOptions /// Gets or sets the lifetime of auto-generated access token, which will be used to authenticate with Azure SignalR Service. /// Default value is one hour. /// - public TimeSpan AccessTokenLifetime { get; set; } = Constants.DefaultAccessTokenLifetime; + public TimeSpan AccessTokenLifetime { get; set; } = Constants.DefaultAccessTokenLifetime; + + /// + /// Gets or sets the access token generate algorithm, supports or + /// Default value is + /// + public AccessTokenAlgorithm AccessTokenAlgorithm { get; set; } = AccessTokenAlgorithm.HS256; /// /// Gets or sets list of endpoints diff --git a/test/Microsoft.Azure.SignalR.AspNet.Tests/ServiceEndpointProviderTests.cs b/test/Microsoft.Azure.SignalR.AspNet.Tests/ServiceEndpointProviderTests.cs index ce95d9965..f14a035ef 100644 --- a/test/Microsoft.Azure.SignalR.AspNet.Tests/ServiceEndpointProviderTests.cs +++ b/test/Microsoft.Azure.SignalR.AspNet.Tests/ServiceEndpointProviderTests.cs @@ -127,6 +127,36 @@ public void TestGenerateServerEndpointWithPrefix(string connectionString, string Assert.Equal(expectedEndpoint, clientEndpoint); } + [Theory] + [InlineData(AccessTokenAlgorithm.HS256)] + [InlineData(AccessTokenAlgorithm.HS512)] + public void TestGenerateServerAccessTokenWithSpecifedAlgorithm(AccessTokenAlgorithm algorithm) + { + var connectionString = "Endpoint=http://localhost;AccessKey=ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789;Port=8080;Version=1.0"; + var provider = new ServiceEndpointProvider(new ServiceEndpoint(connectionString), new ServiceOptions() { AccessTokenAlgorithm = algorithm }); + var generatedToken = provider.GenerateServerAccessToken("hub1", "user1"); + + var handler = new JwtSecurityTokenHandler(); + var token = handler.ReadJwtToken(generatedToken); + + Assert.Equal(algorithm.ToString(), token.SignatureAlgorithm); + } + + [Theory] + [InlineData(AccessTokenAlgorithm.HS256)] + [InlineData(AccessTokenAlgorithm.HS512)] + public void TestGenerateClientAccessTokenWithSpecifedAlgorithm(AccessTokenAlgorithm algorithm) + { + var connectionString = "Endpoint=http://localhost;AccessKey=ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789;Port=8080;Version=1.0"; + var provider = new ServiceEndpointProvider(new ServiceEndpoint(connectionString), new ServiceOptions() { AccessTokenAlgorithm = algorithm }); + var generatedToken = provider.GenerateClientAccessToken("hub1"); + + var handler = new JwtSecurityTokenHandler(); + var token = handler.ReadJwtToken(generatedToken); + + Assert.Equal(algorithm.ToString(), token.SignatureAlgorithm); + } + [Fact(Skip = "Access token does not need to be unique")] public void GenerateMutlipleAccessTokenShouldBeUnique() { diff --git a/test/Microsoft.Azure.SignalR.Common.Tests/AuthenticationHelperTest.cs b/test/Microsoft.Azure.SignalR.Common.Tests/AuthenticationHelperTest.cs index 563718228..c4d9e75c8 100644 --- a/test/Microsoft.Azure.SignalR.Common.Tests/AuthenticationHelperTest.cs +++ b/test/Microsoft.Azure.SignalR.Common.Tests/AuthenticationHelperTest.cs @@ -21,7 +21,7 @@ public class AuthenticationHelperTest public void TestAccessTokenTooLongThrowsException() { var claims = GenerateClaims(100); - var exception = Assert.Throws(() => AuthenticationHelper.GenerateAccessToken(SigningKey, Audience, claims, DefaultLifetime)); + var exception = Assert.Throws(() => AuthenticationHelper.GenerateAccessToken(SigningKey, Audience, claims, DefaultLifetime, AccessTokenAlgorithm.HS256)); Assert.Equal("AccessToken must not be longer than 4K.", exception.Message); } diff --git a/test/Microsoft.Azure.SignalR.Tests/ServiceEndpointProviderFacts.cs b/test/Microsoft.Azure.SignalR.Tests/ServiceEndpointProviderFacts.cs index ac6bbdd4f..a4d90d4ce 100644 --- a/test/Microsoft.Azure.SignalR.Tests/ServiceEndpointProviderFacts.cs +++ b/test/Microsoft.Azure.SignalR.Tests/ServiceEndpointProviderFacts.cs @@ -204,5 +204,31 @@ internal void GenerateClientAccessTokenWithPrefix(IServiceEndpointProvider provi Assert.Equal(expectedTokenString, tokenString); } + + [Theory] + [InlineData(AccessTokenAlgorithm.HS256)] + [InlineData(AccessTokenAlgorithm.HS512)] + public void GenerateServerAccessTokenWithSpecifedAlgorithm(AccessTokenAlgorithm algorithm) + { + var provider = new ServiceEndpointProvider(new ServiceEndpoint(ConnectionStringWithV1Version), new ServiceOptions() { AccessTokenAlgorithm = algorithm }); + var generatedToken = provider.GenerateServerAccessToken("hub1", "user1"); + + var token = JwtTokenHelper.JwtHandler.ReadJwtToken(generatedToken); + + Assert.Equal(algorithm.ToString(), token.SignatureAlgorithm); + } + + [Theory] + [InlineData(AccessTokenAlgorithm.HS256)] + [InlineData(AccessTokenAlgorithm.HS512)] + public void GenerateClientAccessTokenWithSpecifedAlgorithm(AccessTokenAlgorithm algorithm) + { + var provider = new ServiceEndpointProvider(new ServiceEndpoint(ConnectionStringWithV1Version), new ServiceOptions() { AccessTokenAlgorithm = algorithm }); + var generatedToken = provider.GenerateClientAccessToken("hub1"); + + var token = JwtTokenHelper.JwtHandler.ReadJwtToken(generatedToken); + + Assert.Equal(algorithm.ToString(), token.SignatureAlgorithm); + } } } From e36f7e65fdd8b655204e6dbbb4d4b242b820e993 Mon Sep 17 00:00:00 2001 From: "Liangying.Wei" Date: Mon, 16 Mar 2020 20:28:15 +0800 Subject: [PATCH 13/14] some code cleanup (#847) --- .../ServiceEndpointProvider.cs | 6 +- .../ServiceOptions.cs | 8 +- .../Constants.cs | 26 +-- .../Endpoints/ServiceEndpoint.cs | 10 +- .../IConnectionRequestIdProvider.cs | 2 +- .../ServiceConnectionContainerBase.cs | 188 +++++++++--------- .../Utilities/AckHandler.cs | 2 - ...AuthenticationHelper.cs => AuthUtility.cs} | 54 ++--- .../RestApiAccessTokenGenerator.cs | 2 +- .../ServiceEndpointProvider.cs | 6 +- .../ServiceOptionsSetup.cs | 8 +- .../Startup/AzureSignalRHostingStartup.cs | 2 +- .../RunAzureSignalRTests.cs | 8 +- .../AuthenticationHelperTest.cs | 4 +- .../JwtTokenHelper.cs | 2 +- 15 files changed, 156 insertions(+), 172 deletions(-) rename src/Microsoft.Azure.SignalR.Common/Utilities/{AuthenticationHelper.cs => AuthUtility.cs} (83%) diff --git a/src/Microsoft.Azure.SignalR.AspNet/EndpointProvider/ServiceEndpointProvider.cs b/src/Microsoft.Azure.SignalR.AspNet/EndpointProvider/ServiceEndpointProvider.cs index 278d6d649..a0f96042a 100644 --- a/src/Microsoft.Azure.SignalR.AspNet/EndpointProvider/ServiceEndpointProvider.cs +++ b/src/Microsoft.Azure.SignalR.AspNet/EndpointProvider/ServiceEndpointProvider.cs @@ -13,7 +13,7 @@ internal class ServiceEndpointProvider : IServiceEndpointProvider { public static readonly string ConnectionStringNotFound = "No connection string was specified. " + - $"Please specify a configuration entry for {Constants.ConnectionStringDefaultKey}, " + + $"Please specify a configuration entry for {Constants.Keys.ConnectionStringDefaultKey}, " + "or explicitly pass one using IAppBuilder.RunAzureSignalR(connectionString) in Startup.ConfigureServices."; private const string ClientPath = "aspnetclient"; @@ -56,7 +56,7 @@ private string GetPrefixedHubName(string applicationName, string hubName) public string GenerateClientAccessToken(string hubName = null, IEnumerable claims = null, TimeSpan? lifetime = null) { var audience = $"{_endpoint}/{ClientPath}"; - return AuthenticationHelper.GenerateAccessToken(_accessKey, audience, claims, lifetime ?? _accessTokenLifetime, _algorithm); + return AuthUtility.GenerateAccessToken(_accessKey, audience, claims, lifetime ?? _accessTokenLifetime, _algorithm); } public string GenerateServerAccessToken(string hubName, string userId, TimeSpan? lifetime = null) @@ -72,7 +72,7 @@ public string GenerateServerAccessToken(string hubName, string userId, TimeSpan? var audience = $"{_endpoint}/{ServerPath}/?hub={GetPrefixedHubName(_appName, hubName)}"; - return AuthenticationHelper.GenerateAccessToken(_accessKey, audience, claims, lifetime ?? _accessTokenLifetime, _algorithm); + return AuthUtility.GenerateAccessToken(_accessKey, audience, claims, lifetime ?? _accessTokenLifetime, _algorithm); } public string GetClientEndpoint(string hubName = null, string originalPath = null, string queryString = null) diff --git a/src/Microsoft.Azure.SignalR.AspNet/ServiceOptions.cs b/src/Microsoft.Azure.SignalR.AspNet/ServiceOptions.cs index 77b554bcb..af4a83286 100644 --- a/src/Microsoft.Azure.SignalR.AspNet/ServiceOptions.cs +++ b/src/Microsoft.Azure.SignalR.AspNet/ServiceOptions.cs @@ -75,11 +75,11 @@ public ServiceOptions() { var setting = ConfigurationManager.ConnectionStrings[i]; - if (setting.Name == Constants.ConnectionStringDefaultKey) + if (setting.Name == Constants.Keys.ConnectionStringDefaultKey) { connectionString = setting.ConnectionString; } - else if (setting.Name.StartsWith(Constants.ConnectionStringKeyPrefix) && !string.IsNullOrEmpty(setting.ConnectionString)) + else if (setting.Name.StartsWith(Constants.Keys.ConnectionStringKeyPrefix) && !string.IsNullOrEmpty(setting.ConnectionString)) { endpoints.Add(new ServiceEndpoint(setting.Name, setting.ConnectionString)); } @@ -90,11 +90,11 @@ public ServiceOptions() { foreach (var key in ConfigurationManager.AppSettings.AllKeys) { - if (key == Constants.ConnectionStringDefaultKey) + if (key == Constants.Keys.ConnectionStringDefaultKey) { connectionString = ConfigurationManager.AppSettings[key]; } - else if (key.StartsWith(Constants.ConnectionStringKeyPrefix)) + else if (key.StartsWith(Constants.Keys.ConnectionStringKeyPrefix)) { var value = ConfigurationManager.AppSettings[key]; if (!string.IsNullOrEmpty(value)) diff --git a/src/Microsoft.Azure.SignalR.Common/Constants.cs b/src/Microsoft.Azure.SignalR.Common/Constants.cs index 863f3dbbd..5d03d6e23 100644 --- a/src/Microsoft.Azure.SignalR.Common/Constants.cs +++ b/src/Microsoft.Azure.SignalR.Common/Constants.cs @@ -7,9 +7,19 @@ namespace Microsoft.Azure.SignalR { internal static class Constants { - public const string ServerStickyModeDefaultKey = "Azure:SignalR:ServerStickyMode"; - public const string ConnectionStringDefaultKey = "Azure:SignalR:ConnectionString"; - public const string ApplicationNameDefaultKey = "Azure:SignalR:ApplicationName"; + public static class Keys + { + public const string ServerStickyModeDefaultKey = "Azure:SignalR:ServerStickyMode"; + public const string ConnectionStringDefaultKey = "Azure:SignalR:ConnectionString"; + public const string ApplicationNameDefaultKey = "Azure:SignalR:ApplicationName"; + public const string AzureSignalREnabledKey = "Azure:SignalR:Enabled"; + + public static readonly string ConnectionStringSecondaryKey = + $"ConnectionStrings:{ConnectionStringDefaultKey}"; + public static readonly string ConnectionStringKeyPrefix = $"{ConnectionStringDefaultKey}:"; + public static readonly string ApplicationNameDefaultKeyPrefix = $"{ApplicationNameDefaultKey}:"; + public static readonly string ConnectionStringSecondaryKeyPrefix = $"{ConnectionStringSecondaryKey}:"; + } public const int DefaultShutdownTimeoutInSeconds = 30; public const int DefaultScaleTimeoutInSeconds = 300; @@ -20,16 +30,6 @@ internal static class Constants public const string AsrsUserAgent = "Asrs-User-Agent"; public const string AsrsInstanceId = "Asrs-Instance-Id"; - public const string AzureSignalREnabledKey = "Azure:SignalR:Enabled"; - - public static readonly string ConnectionStringSecondaryKey = - $"ConnectionStrings:{ConnectionStringDefaultKey}"; - - public static readonly string ConnectionStringKeyPrefix = $"{ConnectionStringDefaultKey}:"; - - public static readonly string ApplicationNameDefaultKeyPrefix = $"{ApplicationNameDefaultKey}:"; - - public static readonly string ConnectionStringSecondaryKeyPrefix = $"{ConnectionStringSecondaryKey}:"; // Default access token lifetime public static readonly TimeSpan DefaultAccessTokenLifetime = TimeSpan.FromHours(1); diff --git a/src/Microsoft.Azure.SignalR.Common/Endpoints/ServiceEndpoint.cs b/src/Microsoft.Azure.SignalR.Common/Endpoints/ServiceEndpoint.cs index bf9eb8083..df00e28f8 100644 --- a/src/Microsoft.Azure.SignalR.Common/Endpoints/ServiceEndpoint.cs +++ b/src/Microsoft.Azure.SignalR.Common/Endpoints/ServiceEndpoint.cs @@ -99,20 +99,20 @@ public override bool Equals(object obj) internal static (string, EndpointType) ParseKey(string key) { - if (key == Constants.ConnectionStringDefaultKey || key == Constants.ConnectionStringSecondaryKey) + if (key == Constants.Keys.ConnectionStringDefaultKey || key == Constants.Keys.ConnectionStringSecondaryKey) { return (string.Empty, EndpointType.Primary); } - if (key.StartsWith(Constants.ConnectionStringKeyPrefix)) + if (key.StartsWith(Constants.Keys.ConnectionStringKeyPrefix)) { // Azure:SignalR:ConnectionString:: - return ParseKeyWithPrefix(key, Constants.ConnectionStringKeyPrefix); + return ParseKeyWithPrefix(key, Constants.Keys.ConnectionStringKeyPrefix); } - if (key.StartsWith(Constants.ConnectionStringSecondaryKey)) + if (key.StartsWith(Constants.Keys.ConnectionStringSecondaryKey)) { - return ParseKeyWithPrefix(key, Constants.ConnectionStringSecondaryKey); + return ParseKeyWithPrefix(key, Constants.Keys.ConnectionStringSecondaryKey); } throw new ArgumentException($"Invalid format: {key}", nameof(key)); diff --git a/src/Microsoft.Azure.SignalR.Common/Interfaces/IConnectionRequestIdProvider.cs b/src/Microsoft.Azure.SignalR.Common/Interfaces/IConnectionRequestIdProvider.cs index 35c4cb376..4aaa9df31 100644 --- a/src/Microsoft.Azure.SignalR.Common/Interfaces/IConnectionRequestIdProvider.cs +++ b/src/Microsoft.Azure.SignalR.Common/Interfaces/IConnectionRequestIdProvider.cs @@ -13,7 +13,7 @@ internal class DefaultConnectionRequestIdProvider : IConnectionRequestIdProvider { public string GetRequestId() { - return AuthenticationHelper.GenerateRequestId(); + return AuthUtility.GenerateRequestId(); } } } diff --git a/src/Microsoft.Azure.SignalR.Common/ServiceConnections/ServiceConnectionContainerBase.cs b/src/Microsoft.Azure.SignalR.Common/ServiceConnections/ServiceConnectionContainerBase.cs index 3bebdb308..e6fbac7f4 100644 --- a/src/Microsoft.Azure.SignalR.Common/ServiceConnections/ServiceConnectionContainerBase.cs +++ b/src/Microsoft.Azure.SignalR.Common/ServiceConnections/ServiceConnectionContainerBase.cs @@ -193,100 +193,6 @@ public void HandleAck(AckMessage ackMessage) _ackHandler.TriggerAck(ackMessage.AckId, (AckStatus)ackMessage.Status); } - /// - /// Create a connection for a specific service connection type - /// - protected IServiceConnection CreateServiceConnectionCore(ServiceConnectionType type) - { - var connection = ServiceConnectionFactory.Create(Endpoint, this, type); - - connection.ConnectionStatusChanged += OnConnectionStatusChanged; - return connection; - } - - protected virtual async Task OnConnectionComplete(IServiceConnection serviceConnection) - { - if (serviceConnection == null) - { - throw new ArgumentNullException(nameof(serviceConnection)); - } - - serviceConnection.ConnectionStatusChanged -= OnConnectionStatusChanged; - - if (serviceConnection.Status == ServiceConnectionStatus.Connected) - { - return; - } - - var index = FixedServiceConnections.IndexOf(serviceConnection); - if (index != -1) - { - await RestartServiceConnectionCoreAsync(index); - } - } - - private void OnStatusChanged(StatusChange obj) - { - var online = obj.NewStatus == ServiceConnectionStatus.Connected; - Endpoint.Online = online; - if (!online) - { - Log.EndpointOffline(Logger, Endpoint); - } - else - { - Log.EndpointOnline(Logger, Endpoint); - } - } - - private void OnConnectionStatusChanged(StatusChange obj) - { - if (obj.NewStatus == ServiceConnectionStatus.Connected && Status != ServiceConnectionStatus.Connected) - { - Status = GetStatus(); - } - else if (obj.NewStatus == ServiceConnectionStatus.Disconnected && Status != ServiceConnectionStatus.Disconnected) - { - Status = GetStatus(); - } - } - - private async Task RestartServiceConnectionCoreAsync(int index) - { - Func> tryNewConnection = async () => - { - var connection = CreateServiceConnectionCore(InitialConnectionType); - ReplaceFixedConnections(index, connection); - - _ = StartCoreAsync(connection); - await connection.ConnectionInitializedTask; - - return connection.Status == ServiceConnectionStatus.Connected; - }; - await _backOffPolicy.CallProbeWithBackOffAsync(tryNewConnection, GetRetryDelay); - } - - internal static TimeSpan GetRetryDelay(int retryCount) - { - // retry count: 0, 1, 2, 3, 4, 5, 6, ... - // delay seconds: 1, 2, 4, 8, 16, 32, 60, ... - if (retryCount > 5) - { - return TimeSpan.FromMinutes(1) + ReconnectInterval; - } - return TimeSpan.FromSeconds(1 << retryCount) + ReconnectInterval; - } - - protected void ReplaceFixedConnections(int index, IServiceConnection serviceConnection) - { - lock (_lock) - { - var newImmutableConnections = FixedServiceConnections.ToList(); - newImmutableConnections[index] = serviceConnection; - FixedServiceConnections = newImmutableConnections; - } - } - public Task ConnectionInitializedTask => Task.WhenAll(from connection in FixedServiceConnections select connection.ConnectionInitializedTask); @@ -352,6 +258,48 @@ public void Dispose() GC.SuppressFinalize(this); } + /// + /// Create a connection for a specific service connection type + /// + protected IServiceConnection CreateServiceConnectionCore(ServiceConnectionType type) + { + var connection = ServiceConnectionFactory.Create(Endpoint, this, type); + + connection.ConnectionStatusChanged += OnConnectionStatusChanged; + return connection; + } + + protected virtual async Task OnConnectionComplete(IServiceConnection serviceConnection) + { + if (serviceConnection == null) + { + throw new ArgumentNullException(nameof(serviceConnection)); + } + + serviceConnection.ConnectionStatusChanged -= OnConnectionStatusChanged; + + if (serviceConnection.Status == ServiceConnectionStatus.Connected) + { + return; + } + + var index = FixedServiceConnections.IndexOf(serviceConnection); + if (index != -1) + { + await RestartServiceConnectionCoreAsync(index); + } + } + + protected void ReplaceFixedConnections(int index, IServiceConnection serviceConnection) + { + lock (_lock) + { + var newImmutableConnections = FixedServiceConnections.ToList(); + newImmutableConnections[index] = serviceConnection; + FixedServiceConnections = newImmutableConnections; + } + } + protected virtual void Dispose(bool disposing) { if (disposing) @@ -400,6 +348,58 @@ protected async Task RemoveConnectionAsync(IServiceConnection c, bool migratable Log.TimeoutWaitingForFinAck(Logger, retry); } + internal static TimeSpan GetRetryDelay(int retryCount) + { + // retry count: 0, 1, 2, 3, 4, 5, 6, ... + // delay seconds: 1, 2, 4, 8, 16, 32, 60, ... + if (retryCount > 5) + { + return TimeSpan.FromMinutes(1) + ReconnectInterval; + } + return TimeSpan.FromSeconds(1 << retryCount) + ReconnectInterval; + } + + private void OnStatusChanged(StatusChange obj) + { + var online = obj.NewStatus == ServiceConnectionStatus.Connected; + Endpoint.Online = online; + if (!online) + { + Log.EndpointOffline(Logger, Endpoint); + } + else + { + Log.EndpointOnline(Logger, Endpoint); + } + } + + private void OnConnectionStatusChanged(StatusChange obj) + { + if (obj.NewStatus == ServiceConnectionStatus.Connected && Status != ServiceConnectionStatus.Connected) + { + Status = GetStatus(); + } + else if (obj.NewStatus == ServiceConnectionStatus.Disconnected && Status != ServiceConnectionStatus.Disconnected) + { + Status = GetStatus(); + } + } + + private async Task RestartServiceConnectionCoreAsync(int index) + { + Func> tryNewConnection = async () => + { + var connection = CreateServiceConnectionCore(InitialConnectionType); + ReplaceFixedConnections(index, connection); + + _ = StartCoreAsync(connection); + await connection.ConnectionInitializedTask; + + return connection.Status == ServiceConnectionStatus.Connected; + }; + await _backOffPolicy.CallProbeWithBackOffAsync(tryNewConnection, GetRetryDelay); + } + private Task WriteToRandomAvailableConnection(ServiceMessage serviceMessage) { return WriteWithRetry(serviceMessage, StaticRandom.Next(-FixedConnectionCount, FixedConnectionCount), FixedConnectionCount); diff --git a/src/Microsoft.Azure.SignalR.Common/Utilities/AckHandler.cs b/src/Microsoft.Azure.SignalR.Common/Utilities/AckHandler.cs index d0a7b4902..e3fbf8c2b 100644 --- a/src/Microsoft.Azure.SignalR.Common/Utilities/AckHandler.cs +++ b/src/Microsoft.Azure.SignalR.Common/Utilities/AckHandler.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Text; using System.Threading; using System.Threading.Tasks; diff --git a/src/Microsoft.Azure.SignalR.Common/Utilities/AuthenticationHelper.cs b/src/Microsoft.Azure.SignalR.Common/Utilities/AuthUtility.cs similarity index 83% rename from src/Microsoft.Azure.SignalR.Common/Utilities/AuthenticationHelper.cs rename to src/Microsoft.Azure.SignalR.Common/Utilities/AuthUtility.cs index e7b2159e8..63e9ee93b 100644 --- a/src/Microsoft.Azure.SignalR.Common/Utilities/AuthenticationHelper.cs +++ b/src/Microsoft.Azure.SignalR.Common/Utilities/AuthUtility.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.IdentityModel.Tokens.Jwt; -using System.Linq; using System.Security.Claims; using System.Text; using Microsoft.Azure.SignalR.Common; @@ -13,7 +12,7 @@ namespace Microsoft.Azure.SignalR { - internal static class AuthenticationHelper + internal static class AuthUtility { private const int MaxTokenLength = 4096; @@ -30,7 +29,25 @@ public static string GenerateJwtBearer( AccessTokenAlgorithm algorithm = AccessTokenAlgorithm.HS256) { var subject = claims == null ? null : new ClaimsIdentity(claims); - return GenerateJwtBearer(issuer, audience, subject, expires, signingKey, issuedAt, notBefore, algorithm); + SigningCredentials credentials = null; + if (!string.IsNullOrEmpty(signingKey)) + { + // Refer: https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/releases/tag/5.5.0 + // From version 5.5.0, SignatureProvider caching is turned On by default, assign KeyId to enable correct cache for same SigningKey + var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(signingKey)); + securityKey.KeyId = signingKey.GetHashCode().ToString(); + credentials = new SigningCredentials(securityKey, GetSecurityAlgorithm(algorithm)); + } + + var token = JwtTokenHandler.CreateJwtSecurityToken( + issuer: issuer, + audience: audience, + subject: subject, + notBefore: notBefore, + expires: expires, + issuedAt: issuedAt, + signingCredentials: credentials); + return JwtTokenHandler.WriteToken(token); } public static string GenerateAccessToken( @@ -63,37 +80,6 @@ public static string GenerateRequestId() return Convert.ToBase64String(BitConverter.GetBytes(Stopwatch.GetTimestamp())); } - private static string GenerateJwtBearer( - string issuer = null, - string audience = null, - ClaimsIdentity subject = null, - DateTime? expires = null, - string signingKey = null, - DateTime? issuedAt = null, - DateTime? notBefore = null, - AccessTokenAlgorithm algorithm = AccessTokenAlgorithm.HS256) - { - SigningCredentials credentials = null; - if (!string.IsNullOrEmpty(signingKey)) - { - // Refer: https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/releases/tag/5.5.0 - // From version 5.5.0, SignatureProvider caching is turned On by default, assign KeyId to enable correct cache for same SigningKey - var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(signingKey)); - securityKey.KeyId = signingKey; - credentials = new SigningCredentials(securityKey, GetSecurityAlgorithm(algorithm)); - } - - var token = JwtTokenHandler.CreateJwtSecurityToken( - issuer: issuer, - audience: audience, - subject: subject, - notBefore: notBefore, - expires: expires, - issuedAt: issuedAt, - signingCredentials: credentials); - return JwtTokenHandler.WriteToken(token); - } - private static string GetSecurityAlgorithm(AccessTokenAlgorithm algorithm) { return algorithm == AccessTokenAlgorithm.HS256 ? diff --git a/src/Microsoft.Azure.SignalR.Management/RestApiAccessTokenGenerator.cs b/src/Microsoft.Azure.SignalR.Management/RestApiAccessTokenGenerator.cs index 918f6bfd7..3fcf1334b 100644 --- a/src/Microsoft.Azure.SignalR.Management/RestApiAccessTokenGenerator.cs +++ b/src/Microsoft.Azure.SignalR.Management/RestApiAccessTokenGenerator.cs @@ -22,7 +22,7 @@ public RestApiAccessTokenGenerator(string accessKey) public string Generate(string audience, TimeSpan? lifetime = null) { - return AuthenticationHelper.GenerateAccessToken(_accessKey, audience, _claims, lifetime ?? Constants.DefaultAccessTokenLifetime, AccessTokenAlgorithm.HS256); + return AuthUtility.GenerateAccessToken(_accessKey, audience, _claims, lifetime ?? Constants.DefaultAccessTokenLifetime, AccessTokenAlgorithm.HS256); } private static string GenerateServerName() diff --git a/src/Microsoft.Azure.SignalR/EndpointProvider/ServiceEndpointProvider.cs b/src/Microsoft.Azure.SignalR/EndpointProvider/ServiceEndpointProvider.cs index aad887341..db9c32114 100644 --- a/src/Microsoft.Azure.SignalR/EndpointProvider/ServiceEndpointProvider.cs +++ b/src/Microsoft.Azure.SignalR/EndpointProvider/ServiceEndpointProvider.cs @@ -12,7 +12,7 @@ internal class ServiceEndpointProvider : IServiceEndpointProvider { public static readonly string ConnectionStringNotFound = "No connection string was specified. " + - $"Please specify a configuration entry for {Constants.ConnectionStringDefaultKey}, " + + $"Please specify a configuration entry for {Constants.Keys.ConnectionStringDefaultKey}, " + "or explicitly pass one using IServiceCollection.AddAzureSignalR(connectionString) in Startup.ConfigureServices."; private readonly string _accessKey; @@ -52,7 +52,7 @@ public string GenerateClientAccessToken(string hubName, IEnumerable claim var audience = _generator.GetClientAudience(hubName, _appName); - return AuthenticationHelper.GenerateAccessToken(_accessKey, audience, claims, lifetime ?? _accessTokenLifetime, _algorithm); + return AuthUtility.GenerateAccessToken(_accessKey, audience, claims, lifetime ?? _accessTokenLifetime, _algorithm); } public string GenerateServerAccessToken(string hubName, string userId, TimeSpan? lifetime = null) @@ -65,7 +65,7 @@ public string GenerateServerAccessToken(string hubName, string userId, TimeSpan? var audience = _generator.GetServerAudience(hubName, _appName); var claims = userId != null ? new[] { new Claim(ClaimTypes.NameIdentifier, userId) } : null; - return AuthenticationHelper.GenerateAccessToken(_accessKey, audience, claims, lifetime ?? _accessTokenLifetime, _algorithm); + return AuthUtility.GenerateAccessToken(_accessKey, audience, claims, lifetime ?? _accessTokenLifetime, _algorithm); } public string GetClientEndpoint(string hubName, string originalPath, string queryString) diff --git a/src/Microsoft.Azure.SignalR/ServiceOptionsSetup.cs b/src/Microsoft.Azure.SignalR/ServiceOptionsSetup.cs index bf741dac5..7a2ecadd3 100644 --- a/src/Microsoft.Azure.SignalR/ServiceOptionsSetup.cs +++ b/src/Microsoft.Azure.SignalR/ServiceOptionsSetup.cs @@ -44,20 +44,20 @@ public IChangeToken GetChangeToken() private (string AppName, string ConnectionString, ServerStickyMode StickyMode, ServiceEndpoint[] Endpoints) ParseConfiguration() { - var appName = _configuration[Constants.ApplicationNameDefaultKeyPrefix]; + var appName = _configuration[Constants.Keys.ApplicationNameDefaultKeyPrefix]; var stickyMode = ServerStickyMode.Disabled; - var mode = _configuration[Constants.ServerStickyModeDefaultKey]; + var mode = _configuration[Constants.Keys.ServerStickyModeDefaultKey]; if (!string.IsNullOrEmpty(mode)) { Enum.TryParse(mode, true, out stickyMode); } - var (connectionString, endpoints) = GetEndpoint(_configuration, Constants.ConnectionStringDefaultKey); + var (connectionString, endpoints) = GetEndpoint(_configuration, Constants.Keys.ConnectionStringDefaultKey); // Fallback to ConnectionStrings:Azure:SignalR:ConnectionString format when the default one is not available if (connectionString == null && endpoints.Length == 0) { - (connectionString, endpoints) = GetEndpoint(_configuration, Constants.ConnectionStringSecondaryKey); + (connectionString, endpoints) = GetEndpoint(_configuration, Constants.Keys.ConnectionStringSecondaryKey); } return (appName, connectionString, stickyMode, endpoints); diff --git a/src/Microsoft.Azure.SignalR/Startup/AzureSignalRHostingStartup.cs b/src/Microsoft.Azure.SignalR/Startup/AzureSignalRHostingStartup.cs index efb58e9ab..05a254094 100644 --- a/src/Microsoft.Azure.SignalR/Startup/AzureSignalRHostingStartup.cs +++ b/src/Microsoft.Azure.SignalR/Startup/AzureSignalRHostingStartup.cs @@ -18,7 +18,7 @@ public void Configure(IWebHostBuilder builder) { builder.ConfigureServices((context, services) => { - if (!context.HostingEnvironment.IsDevelopment() || context.Configuration.GetSection(Constants.AzureSignalREnabledKey).Get()) + if (!context.HostingEnvironment.IsDevelopment() || context.Configuration.GetSection(Constants.Keys.AzureSignalREnabledKey).Get()) { services.AddSignalR().AddAzureSignalR(); } diff --git a/test/Microsoft.Azure.SignalR.AspNet.Tests/RunAzureSignalRTests.cs b/test/Microsoft.Azure.SignalR.AspNet.Tests/RunAzureSignalRTests.cs index db4a89c94..055f0b367 100644 --- a/test/Microsoft.Azure.SignalR.AspNet.Tests/RunAzureSignalRTests.cs +++ b/test/Microsoft.Azure.SignalR.AspNet.Tests/RunAzureSignalRTests.cs @@ -780,13 +780,13 @@ private sealed class AppSettingsConfigScope : IDisposable public AppSettingsConfigScope(string setting, params string[] additionalSettings) { - _originalSetting = ConfigurationManager.AppSettings[Constants.ConnectionStringDefaultKey]; - ConfigurationManager.AppSettings[Constants.ConnectionStringDefaultKey] = setting; + _originalSetting = ConfigurationManager.AppSettings[Constants.Keys.ConnectionStringDefaultKey]; + ConfigurationManager.AppSettings[Constants.Keys.ConnectionStringDefaultKey] = setting; var newSettings = additionalSettings.Select( s => new KeyValuePair( - Constants.ConnectionStringKeyPrefix + Guid.NewGuid().ToString("N") + Constants.Keys.ConnectionStringKeyPrefix + Guid.NewGuid().ToString("N") , s)) .ToList(); _originalAdditonalSettings = newSettings.Select(s => @@ -800,7 +800,7 @@ public AppSettingsConfigScope(string setting, params string[] additionalSettings public void Dispose() { - ConfigurationManager.AppSettings[Constants.ConnectionStringDefaultKey] = _originalSetting; + ConfigurationManager.AppSettings[Constants.Keys.ConnectionStringDefaultKey] = _originalSetting; foreach (var pair in _originalAdditonalSettings) { ConfigurationManager.AppSettings[pair.Key] = pair.Value; diff --git a/test/Microsoft.Azure.SignalR.Common.Tests/AuthenticationHelperTest.cs b/test/Microsoft.Azure.SignalR.Common.Tests/AuthenticationHelperTest.cs index c4d9e75c8..77410cfb3 100644 --- a/test/Microsoft.Azure.SignalR.Common.Tests/AuthenticationHelperTest.cs +++ b/test/Microsoft.Azure.SignalR.Common.Tests/AuthenticationHelperTest.cs @@ -21,7 +21,7 @@ public class AuthenticationHelperTest public void TestAccessTokenTooLongThrowsException() { var claims = GenerateClaims(100); - var exception = Assert.Throws(() => AuthenticationHelper.GenerateAccessToken(SigningKey, Audience, claims, DefaultLifetime, AccessTokenAlgorithm.HS256)); + var exception = Assert.Throws(() => AuthUtility.GenerateAccessToken(SigningKey, Audience, claims, DefaultLifetime, AccessTokenAlgorithm.HS256)); Assert.Equal("AccessToken must not be longer than 4K.", exception.Message); } @@ -32,7 +32,7 @@ public void TestGenerateJwtBearerCaching() var count = 0; while (count < 1000) { - AuthenticationHelper.GenerateJwtBearer(audience: Audience, expires: DateTime.UtcNow.Add(DefaultLifetime), signingKey: SigningKey); + AuthUtility.GenerateJwtBearer(audience: Audience, expires: DateTime.UtcNow.Add(DefaultLifetime), signingKey: SigningKey); count++; }; diff --git a/test/Microsoft.Azure.SignalR.Tests/JwtTokenHelper.cs b/test/Microsoft.Azure.SignalR.Tests/JwtTokenHelper.cs index daa5ed970..b8588217f 100644 --- a/test/Microsoft.Azure.SignalR.Tests/JwtTokenHelper.cs +++ b/test/Microsoft.Azure.SignalR.Tests/JwtTokenHelper.cs @@ -50,7 +50,7 @@ public static string GenerateJwtBearer(string audience, string requestId) { - return AuthenticationHelper.GenerateJwtBearer( + return AuthUtility.GenerateJwtBearer( issuer: null, audience: audience, claims: subject, From 9c576db9d42b2158625bfcb0d05544a5b1b72e8b Mon Sep 17 00:00:00 2001 From: Terence Fan Date: Mon, 16 Mar 2020 20:39:03 +0800 Subject: [PATCH 14/14] Remove accessKey field from generator constructor (#844) Co-authored-by: Liangying.Wei --- .../EndpointProvider/DefaultServiceEndpointGenerator.cs | 5 +---- .../EndpointProvider/ServiceEndpointProvider.cs | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.Azure.SignalR/EndpointProvider/DefaultServiceEndpointGenerator.cs b/src/Microsoft.Azure.SignalR/EndpointProvider/DefaultServiceEndpointGenerator.cs index adaba65ad..e34f82a24 100644 --- a/src/Microsoft.Azure.SignalR/EndpointProvider/DefaultServiceEndpointGenerator.cs +++ b/src/Microsoft.Azure.SignalR/EndpointProvider/DefaultServiceEndpointGenerator.cs @@ -13,16 +13,13 @@ internal sealed class DefaultServiceEndpointGenerator : IServiceEndpointGenerato public string Endpoint { get; } - public string AccessKey { get; } - public string Version { get; } public int? Port { get; } - public DefaultServiceEndpointGenerator(string endpoint, string accessKey, string version, int? port) + public DefaultServiceEndpointGenerator(string endpoint, string version, int? port) { Endpoint = endpoint; - AccessKey = accessKey; Version = version; Port = port; } diff --git a/src/Microsoft.Azure.SignalR/EndpointProvider/ServiceEndpointProvider.cs b/src/Microsoft.Azure.SignalR/EndpointProvider/ServiceEndpointProvider.cs index db9c32114..baa79767e 100644 --- a/src/Microsoft.Azure.SignalR/EndpointProvider/ServiceEndpointProvider.cs +++ b/src/Microsoft.Azure.SignalR/EndpointProvider/ServiceEndpointProvider.cs @@ -40,7 +40,7 @@ public ServiceEndpointProvider(ServiceEndpoint endpoint, ServiceOptions serviceO var port = endpoint.Port; var version = endpoint.Version; - _generator = new DefaultServiceEndpointGenerator(endpoint.Endpoint, _accessKey, version, port); + _generator = new DefaultServiceEndpointGenerator(endpoint.Endpoint, version, port); } public string GenerateClientAccessToken(string hubName, IEnumerable claims = null, TimeSpan? lifetime = null)