From 6f7a5dafe9a900ca9476bb09f83e2cb70f16a32b Mon Sep 17 00:00:00 2001 From: stdrickforce Date: Fri, 20 Sep 2019 16:13:41 +0800 Subject: [PATCH] Add MigrationLevel to HandshakeRequest --- specs/ServiceProtocol.md | 22 ++++- .../ServerConnectionMigrationLevel.cs | 9 ++ .../ServiceMessage.cs | 12 +++ .../ServiceProtocol.cs | 26 ++--- .../ServiceProtocolConstants.cs | 2 +- src/Microsoft.Azure.SignalR/ServiceOptions.cs | 10 +- ...osoft.Azure.SignalR.Protocols.Tests.csproj | 2 +- .../ServiceProtocolFacts.cs | 96 +++++++++++++------ 8 files changed, 126 insertions(+), 53 deletions(-) create mode 100644 src/Microsoft.Azure.SignalR.Common/ServiceConnections/ServerConnectionMigrationLevel.cs diff --git a/specs/ServiceProtocol.md b/specs/ServiceProtocol.md index be60ea67a..b57f0b8ec 100644 --- a/specs/ServiceProtocol.md +++ b/specs/ServiceProtocol.md @@ -5,7 +5,7 @@ The Azure SignalR Service Protocol is a protocol between Azure SignalR Service a ## Terms - Service - Azure SignalR Service. It accepts connections from both clients and servers, acting as the abstract transport between them. It will internally maintain a one-to-one mapping between clients and servers, to make sure that messages are correctly routed to the recipients as if it is a physical transport. -- Server - Application server node, which is connected to the Azure SignalR Service, using this protocol to receive data from and send data to clients through Azure SignalR Service. +- Server - Application server node, which is connected to the Azure SignalR Service, using this protocol to receive data from and send data to clients through Azure SignalR Service. - Client - The SignalR client connected to the Azure SignalR Service. The Azure SignalR Service will look exactly the same as a self-hosted SignalR server from the client's perspective. ## Overview @@ -53,7 +53,7 @@ Ack | Service | Sent from Service to Server to return the operation result of Jo ## Communication Model -This protocol will be used between Service and Server. There will be one or a few physical connections between Service and Server. Data from/to multiple client connections will be multiplexed within these physical connections. Each client connection will be identified by a unique connection Id. +This protocol will be used between Service and Server. There will be one or a few physical connections between Service and Server. Data from/to multiple client connections will be multiplexed within these physical connections. Each client connection will be identified by a unique connection Id. The number of client connections will be far more (over 100 times) than the number of physical connections between Service and Server. @@ -67,15 +67,23 @@ Server will initiate a physical connection to Service, using WebSocket transport When a new client is connected to Service, a `OpenConnection` message will be sent by Service to Server. +#### Client migrate-in from another server + +When a new client is migrated from another server, a `OpenConnection` message will be sent by Service to Server, with an `Asrs-Migrated-In` header given. + ### Client Disconnect - When a client is disconnected from Service, a `CloseConnection` message will be sent by Service to Server. - When Server wants to disconnect a client, a `CloseConnection` message will be sent by Server to Service. Then Service will disconnect the phyical connection with the target client. +#### Client migrate-out to another server + +When a client is migrated to another server, a `CloseConnection` message will be sent by Service to Server, with an `Asrs-Migrated-Out` header given. + ### Client Data Pass Through - When a client sends data to Service, a `ConnectionData` message will be sent by Service to Server. -- When Server wants to send data to a client, a `ConnectionData` message will be sent by Server to Service. +- When Server wants to send data to a client, a `ConnectionData` message will be sent by Server to Service. ### SignalR scenarios @@ -100,6 +108,14 @@ MessagePack uses different formats to encode values. Refer to the [MessagePack F ``` - 1 - Message Type, indicating this is a `HandshakeRequest` message. - Version - A `Int32` encoding number of the protocol version. +- ConnectionType - A `Int32` encoding number of the connection type. + - 0, Default, it can carry clients, service runtime should always accept this kind of connection. + - 1, OnDemand, creating when service requested more connections, it can carry clients, but it may be rejected by service runtime. + - 2, Weak, it can not carry clients, but it can send message. +- MigrationLevel - A `Int32` encoding number indicates if further client connections associated with this server connection could be migrated. + - 0, Off, a client connection can not be migrated to another server. + - 1, ShutdownOnly, a client connection can be migrated only if the matched server was shutdown gracefully. + - 2, Any, a client connection can be migrated even if the matched server connection was dropped accidentally. (may cause data loss) #### Example: TODO diff --git a/src/Microsoft.Azure.SignalR.Common/ServiceConnections/ServerConnectionMigrationLevel.cs b/src/Microsoft.Azure.SignalR.Common/ServiceConnections/ServerConnectionMigrationLevel.cs new file mode 100644 index 000000000..aea2280df --- /dev/null +++ b/src/Microsoft.Azure.SignalR.Common/ServiceConnections/ServerConnectionMigrationLevel.cs @@ -0,0 +1,9 @@ +namespace Microsoft.Azure.SignalR +{ + public enum ServerConnectionMigrationLevel + { + Off = 0, + ShutdownOnly = 1, + Any = 2, + } +} diff --git a/src/Microsoft.Azure.SignalR.Protocols/ServiceMessage.cs b/src/Microsoft.Azure.SignalR.Protocols/ServiceMessage.cs index 7e2a9f6c5..94719d909 100644 --- a/src/Microsoft.Azure.SignalR.Protocols/ServiceMessage.cs +++ b/src/Microsoft.Azure.SignalR.Protocols/ServiceMessage.cs @@ -42,6 +42,18 @@ public class HandshakeRequestMessage : ServiceMessage /// public int ConnectionType { get; set; } + /// + /// Gets or sets the migratable flag. + /// + /// + /// 0, Off, a client connection can not be migrated to another server. + /// 1, ShutdownOnly, a client connection can be migrated only if the pairing server was shutdown gracefully. + /// 2, Any, a client connection can be migrated even if the pairing server connection was dropped accidentally. (may cause data loss) + /// + /// + /// + public int MigrationLevel { get; set; } + /// /// Gets or sets the target of service connection, only work for OnDemand connections. /// diff --git a/src/Microsoft.Azure.SignalR.Protocols/ServiceProtocol.cs b/src/Microsoft.Azure.SignalR.Protocols/ServiceProtocol.cs index 46dcd7ad6..cee18a21b 100644 --- a/src/Microsoft.Azure.SignalR.Protocols/ServiceProtocol.cs +++ b/src/Microsoft.Azure.SignalR.Protocols/ServiceProtocol.cs @@ -227,20 +227,12 @@ private static void WriteMessageCore(ServiceMessage message, Stream packer) private static void WriteHandshakeRequestMessage(HandshakeRequestMessage message, Stream packer) { - if (message.ConnectionType == 0) - { - MessagePackBinary.WriteArrayHeader(packer, 2); - MessagePackBinary.WriteInt32(packer, ServiceProtocolConstants.HandshakeRequestType); - MessagePackBinary.WriteInt32(packer, message.Version); - } - else - { - MessagePackBinary.WriteArrayHeader(packer, 4); - MessagePackBinary.WriteInt32(packer, ServiceProtocolConstants.HandshakeRequestType); - MessagePackBinary.WriteInt32(packer, message.Version); - MessagePackBinary.WriteInt32(packer, message.ConnectionType); - MessagePackBinary.WriteString(packer, message.Target ?? string.Empty); - } + MessagePackBinary.WriteArrayHeader(packer, 5); + MessagePackBinary.WriteInt32(packer, ServiceProtocolConstants.HandshakeRequestType); + MessagePackBinary.WriteInt32(packer, message.Version); + MessagePackBinary.WriteInt32(packer, message.ConnectionType); + MessagePackBinary.WriteString(packer, message.ConnectionType == 0 ? "" : message.Target ?? string.Empty); + MessagePackBinary.WriteInt32(packer, (int) message.MigrationLevel); } private static void WriteHandshakeResponseMessage(HandshakeResponseMessage message, Stream packer) @@ -516,7 +508,8 @@ private static HandshakeRequestMessage CreateHandshakeRequestMessage(byte[] inpu { result.ConnectionType = ReadInt32(input, ref offset, "connectionType"); result.Target = ReadString(input, ref offset, "target"); - } + } + result.MigrationLevel = arrayLength >= 5 ? ReadInt32(input, ref offset, "migratableStatus") : 0; return result; } @@ -553,7 +546,6 @@ private static OpenConnectionMessage CreateOpenConnectionMessage(int arrayLength { var headers = ReadHeaders(input, ref offset); var queryString = ReadString(input, ref offset, "queryString"); - return new OpenConnectionMessage(connectionId, claims, headers, queryString); } else @@ -575,7 +567,7 @@ private static ConnectionDataMessage CreateConnectionDataMessage(byte[] input, r var connectionId = ReadString(input, ref offset, "connectionId"); var payload = ReadBytes(input, ref offset, "payload"); - return new ConnectionDataMessage(connectionId, payload); + return new ConnectionDataMessage(connectionId, payload); } private static MultiConnectionDataMessage CreateMultiConnectionDataMessage(byte[] input, ref int offset) diff --git a/src/Microsoft.Azure.SignalR.Protocols/ServiceProtocolConstants.cs b/src/Microsoft.Azure.SignalR.Protocols/ServiceProtocolConstants.cs index 400560bcc..3bc0f2520 100644 --- a/src/Microsoft.Azure.SignalR.Protocols/ServiceProtocolConstants.cs +++ b/src/Microsoft.Azure.SignalR.Protocols/ServiceProtocolConstants.cs @@ -25,5 +25,5 @@ public static class ServiceProtocolConstants public const int JoinGroupWithAckMessageType = 18; public const int LeaveGroupWithAckMessageType = 19; public const int AckMessageType = 20; - } + } } diff --git a/src/Microsoft.Azure.SignalR/ServiceOptions.cs b/src/Microsoft.Azure.SignalR/ServiceOptions.cs index be45768de..83c9099ca 100644 --- a/src/Microsoft.Azure.SignalR/ServiceOptions.cs +++ b/src/Microsoft.Azure.SignalR/ServiceOptions.cs @@ -65,7 +65,15 @@ public class ServiceOptions : IServiceEndpointOptions internal TimeSpan ServerShutdownTimeout { get; set; } = TimeSpan.FromSeconds(Constants.DefaultShutdownTimeoutInSeconds); /// - /// Gets or sets the proxy used when ServiceEndpoint will attempt to connect to Azure SignalR Service. + /// Specifies if the client-connection assigned to this server can be migrated to another server. + /// Default value is 0. + /// 1: Only migrate client-connection if server was shutdown gracefully. + /// 2: Migrate client-connection even if server-connection was accidentally dropped. (Potential data losses) + /// + internal ServerConnectionMigrationLevel MigrationLevel { get; set; } = ServerConnectionMigrationLevel.Off; + + /// + /// Gets or sets the proxy used when ServiceEndpoint will attempt to connect to Azure SignalR. /// public IWebProxy Proxy { get; set; } } diff --git a/test/Microsoft.Azure.SignalR.Protocols.Tests/Microsoft.Azure.SignalR.Protocols.Tests.csproj b/test/Microsoft.Azure.SignalR.Protocols.Tests/Microsoft.Azure.SignalR.Protocols.Tests.csproj index a375191eb..fc6cfb85e 100644 --- a/test/Microsoft.Azure.SignalR.Protocols.Tests/Microsoft.Azure.SignalR.Protocols.Tests.csproj +++ b/test/Microsoft.Azure.SignalR.Protocols.Tests/Microsoft.Azure.SignalR.Protocols.Tests.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.0 diff --git a/test/Microsoft.Azure.SignalR.Protocols.Tests/ServiceProtocolFacts.cs b/test/Microsoft.Azure.SignalR.Protocols.Tests/ServiceProtocolFacts.cs index 11f2a31f4..ec52bcdb6 100644 --- a/test/Microsoft.Azure.SignalR.Protocols.Tests/ServiceProtocolFacts.cs +++ b/test/Microsoft.Azure.SignalR.Protocols.Tests/ServiceProtocolFacts.cs @@ -19,7 +19,7 @@ public static IEnumerable TestWriteData { get { - foreach (var k in TestData.Values) + foreach (var k in TestData.Keys) { yield return new object[] { k }; } @@ -30,39 +30,32 @@ public static IEnumerable TestParseData { get { - foreach (var k in TestCompacityData.Values) - { - yield return new object[] { k }; - } - foreach (var k in TestData.Values) + foreach (var k in TestData.Keys) { yield return new object[] { k }; } } } - public static IDictionary TestCompacityData => new[] - { - new ProtocolTestData( - name: "CloseConnection", - message: new CloseConnectionMessage("conn3"), - binary: "kwWlY29ubjPA"), - new ProtocolTestData( - name: "CloseConnectionWithError", - message: new CloseConnectionMessage("conn4", "Error message."), - binary: "kwWlY29ubjSuRXJyb3IgbWVzc2FnZS4="), - }.ToDictionary(t => t.Name); - - public static IDictionary TestData => new[] + public static IEnumerable TestParseOldData { - new ProtocolTestData( - name: "CloseConnection", - message: new CloseConnectionMessage("conn3"), - binary: "lAWlY29ubjOggA=="), - new ProtocolTestData( - name: "CloseConnectionWithError", - message: new CloseConnectionMessage("conn4", "Error message."), - binary: "lAWlY29ubjSuRXJyb3IgbWVzc2FnZS6A"), + get + { + foreach (var k in TestCompatibilityData.Keys) + { + yield return new object[] { k }; + } + } + } + public static IDictionary TestCompatibilityData => new[] { + new ProtocolTestData( + name: "CloseConnection", + message: new CloseConnectionMessage("conn3"), + binary: "kwWlY29ubjPA"), + new ProtocolTestData( + name: "CloseConnectionWithError", + message: new CloseConnectionMessage("conn4", "Error message."), + binary: "kwWlY29ubjSuRXJyb3IgbWVzc2FnZS4="), new ProtocolTestData( name: "CloseConnectionWithHeaders", message: new CloseConnectionMessage("conn4", "Error message.", new Dictionary() { @@ -77,6 +70,22 @@ public static IEnumerable TestParseData name: "HandshakeRequestWithProperty", message: new HandshakeRequestMessage(1) { ConnectionType = 1, Target = "abc" }, binary: "lAEBAaNhYmM="), + }.ToDictionary(t => t.Name); + + public static IDictionary TestData => new[] + { + new ProtocolTestData( + name: "HandshakeRequest", + message: new HandshakeRequestMessage(1), + binary: "lQEBAKAA"), + new ProtocolTestData( + name: "HandshakeRequestWithProperty", + message: new HandshakeRequestMessage(1) { ConnectionType = 1, Target = "abc" }, + binary: "lQEBAaNhYmMA"), + new ProtocolTestData( + name: "HandshakeRequestWithMigratableStatus", + message: new HandshakeRequestMessage(1) { MigrationLevel = 1}, + binary: "lQEBAKAB"), new ProtocolTestData( name: "HandshakeResponse", message: new HandshakeResponseMessage(), @@ -118,6 +127,14 @@ public static IEnumerable TestParseData name: "OpenConnectionWithQueryString2", message: new OpenConnectionMessage("conn4", null, new Dictionary(), "query1=value1&query2=query2&query3=value3"), binary: "lQSlY29ubjSAgNkpcXVlcnkxPXZhbHVlMSZxdWVyeTI9cXVlcnkyJnF1ZXJ5Mz12YWx1ZTM="), + new ProtocolTestData( + name: "CloseConnection", + message: new CloseConnectionMessage("conn3"), + binary: "lAWlY29ubjOggA=="), + new ProtocolTestData( + name: "CloseConnectionWithError", + message: new CloseConnectionMessage("conn4", "Error message."), + binary: "lAWlY29ubjSuRXJyb3IgbWVzc2FnZS6A"), new ProtocolTestData( name: "ConnectionData", message: new ConnectionDataMessage("conn5", new byte[] {1, 2, 3, 4, 5, 6, 7}), @@ -231,9 +248,26 @@ public static IEnumerable TestParseData }.ToDictionary(t => t.Name); [Theory] - [MemberData(nameof(TestParseData))] - public void ParseMessages(ProtocolTestData testData) + [MemberData(nameof(TestParseOldData))] + public void ParseOldMessages(string testDataName) { + var testData = TestCompatibilityData[testDataName]; + + // Verify that the input binary string decodes to the expected MsgPack primitives + var bytes = Convert.FromBase64String(testData.Binary); + + // Parse the input fully now. + bytes = Frame(bytes); + var message = ParseServiceMessage(bytes); + Assert.Equal(testData.Message, message, ServiceMessageEqualityComparer.Instance); + } + + [Theory] + [MemberData(nameof(TestParseData))] + public void ParseMessages(string testDataName) + { + var testData = TestData[testDataName]; + // Verify that the input binary string decodes to the expected MsgPack primitives var bytes = Convert.FromBase64String(testData.Binary); @@ -245,8 +279,10 @@ public void ParseMessages(ProtocolTestData testData) [Theory] [MemberData(nameof(TestWriteData))] - public void WriteMessages(ProtocolTestData testData) + public void WriteMessages(string testDataName) { + var testData = TestData[testDataName]; + var bytes = Protocol.GetMessageBytes(testData.Message); // Unframe the message to check the binary encoding