From fef1da6abc331aed29e32ad873436fcd068a10e5 Mon Sep 17 00:00:00 2001 From: Apolixit Date: Thu, 4 Jul 2024 22:23:26 +0200 Subject: [PATCH 1/6] Add OnConnectionSet and OnConnectionLost to the Substrate Websocket --- .../Substrate.NetApi.Test.csproj | 1 + Substrate.NetApi.TestNode/ClientTests.cs | 68 +++++++++++++++++ Substrate.NetApi/SubstrateClient.cs | 73 +++++++++++++++++-- 3 files changed, 134 insertions(+), 8 deletions(-) create mode 100644 Substrate.NetApi.TestNode/ClientTests.cs diff --git a/Substrate.NetApi.Test/Substrate.NetApi.Test.csproj b/Substrate.NetApi.Test/Substrate.NetApi.Test.csproj index f42a896..b2d8207 100644 --- a/Substrate.NetApi.Test/Substrate.NetApi.Test.csproj +++ b/Substrate.NetApi.Test/Substrate.NetApi.Test.csproj @@ -7,6 +7,7 @@ + diff --git a/Substrate.NetApi.TestNode/ClientTests.cs b/Substrate.NetApi.TestNode/ClientTests.cs new file mode 100644 index 0000000..c4bef23 --- /dev/null +++ b/Substrate.NetApi.TestNode/ClientTests.cs @@ -0,0 +1,68 @@ +using NUnit.Framework; +using Substrate.NetApi.Model.Extrinsics; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Sockets; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Substrate.NetApi.TestNode +{ + public class ClientTests + { + private SubstrateClient _client; + + [SetUp] + public void Setup() + { + _client = new SubstrateClient(new Uri("ws://rpc-parachain.bajun.network"), ChargeTransactionPayment.Default()); + } + + [Test] + public async Task Connect_ShouldConnectSuccessfullyAsync() + { + Assert.That(_client.IsConnected, Is.False); + + await _client.ConnectAsync(); + Assert.That(_client.IsConnected, Is.True); + } + + [Test] + public async Task Connect_ShouldDisconnectSuccessfullyAsync() + { + await _client.ConnectAsync(); + Assert.That(_client.IsConnected, Is.True); + + await _client.CloseAsync(); + Assert.That(_client.IsConnected, Is.False); + } + + [Test] + public async Task Connect_ShouldTriggerEventAsync() + { + var onConnectionSetTriggered = new TaskCompletionSource(); + _client.ConnectionSet += (sender, e) => onConnectionSetTriggered.SetResult(true); + + await _client.ConnectAsync(); + + await Task.WhenAny(onConnectionSetTriggered.Task, Task.Delay(TimeSpan.FromMinutes(1))); + Assert.That(onConnectionSetTriggered.Task.IsCompleted, Is.True); + } + + [Test] + public async Task OnConnectionLost_ShouldThrowDisconnectedEventAsync() + { + var onConnectionLostTriggered = new TaskCompletionSource(); + _client.ConnectionLost += (sender, e) => onConnectionLostTriggered.SetResult(true); + + await _client.ConnectAsync(); + await _client.CloseAsync(); + + await Task.WhenAny(onConnectionLostTriggered.Task, Task.Delay(TimeSpan.FromMinutes(1))); + Assert.That(onConnectionLostTriggered.Task.IsCompleted, Is.True); + } + } +} diff --git a/Substrate.NetApi/SubstrateClient.cs b/Substrate.NetApi/SubstrateClient.cs index fc1035e..19910e3 100644 --- a/Substrate.NetApi/SubstrateClient.cs +++ b/Substrate.NetApi/SubstrateClient.cs @@ -55,7 +55,22 @@ public class SubstrateClient : IDisposable private ClientWebSocket _socket; /// - /// Bypass Remote Certificate Validation. Useful when testing with self-signed SSL certificates. + /// Task to received connection state + /// + private Task _receivedTask; + + /// + /// The connexion lost event trigger when the websocket change state to disconnected + /// + public event EventHandler ConnectionLost; + + /// + /// Event triggered when the connection is set + /// + public event EventHandler ConnectionSet; + + /// + /// Bypass Remote Certificate Validation. Useful when testing with self-signed SSL certificates. /// private readonly bool _bypassRemoteCertificateValidation; @@ -88,7 +103,7 @@ public SubstrateClient(Uri uri, ChargeType chargeType, bool bypassRemoteCertific /// Information describing the meta. public MetaData MetaData { get; private set; } - /// + /// /// Network runtime version /// public RuntimeVersion RuntimeVersion { get; private set; } @@ -205,7 +220,7 @@ public async Task ConnectAsync(bool useMetaData, bool standardSubstrate, Cancell #if NETSTANDARD2_0 throw new NotSupportedException("Bypass remote certification validation not supported in NETStandard2.0"); #elif NETSTANDARD2_1_OR_GREATER - _socket.Options.RemoteCertificateValidationCallback = (sender, certificate, chain, errors) => true; + _socket.Options.RemoteCertificateValidationCallback = (sender, certificate, chain, errors) => true; #endif } } @@ -213,9 +228,13 @@ public async Task ConnectAsync(bool useMetaData, bool standardSubstrate, Cancell _connectTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30)); var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token, _connectTokenSource.Token); await _socket.ConnectAsync(_uri, linkedTokenSource.Token); + + // Triger the event + OnConnectionSet(); + linkedTokenSource.Dispose(); - _connectTokenSource.Dispose(); - _connectTokenSource = null; + //_connectTokenSource.Dispose(); + //_connectTokenSource = null; Logger.Debug("Connected to Websocket."); var formatter = new JsonMessageFormatter(); @@ -270,9 +289,47 @@ public async Task ConnectAsync(bool useMetaData, bool standardSubstrate, Cancell } } + _receivedTask = Task.Run(CheckStateAsync); //_jsonRpc.TraceSource.Switch.Level = SourceLevels.All; } + /// + /// Continuously checks the state of the WebSocket connection in an asynchronous loop. + /// + private async Task CheckStateAsync() { + var buffer = new byte[1024 * 4]; + + try { + while(_socket.State == WebSocketState.Open) { + var result = await _socket.ReceiveAsync(new ArraySegment(buffer), _connectTokenSource.Token); + + if(result.MessageType == WebSocketMessageType.Close) { + await _socket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); + + OnConnectionLost(); + } + } + } catch(WebSocketException) { + // Maybe check : ex.WebSocketErrorCode + OnConnectionLost(); + } + } + + /// + /// Raises the event when the connection to the server is lost. + /// + protected virtual void OnConnectionLost() { + ConnectionLost?.Invoke(this, EventArgs.Empty); + } + + /// + /// Raises the event when the connection to the server is set. + /// + protected virtual void OnConnectionSet() + { + ConnectionSet?.Invoke(this, EventArgs.Empty); + } + /// /// Gets the storage asynchronous for generated code. /// @@ -446,9 +503,7 @@ public async Task CloseAsync() /// An asynchronous result. public async Task CloseAsync(CancellationToken token) { - _connectTokenSource?.Cancel(); - - await Task.Run(() => + await Task.Run(async () => { // cancel remaining request tokens foreach (var key in _requestTokenSourceDict.Keys) key?.Cancel(); @@ -456,6 +511,8 @@ await Task.Run(() => if (_socket != null && _socket.State == WebSocketState.Open) { + await _socket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); + _connectTokenSource?.Cancel(); _jsonRpc?.Dispose(); Logger.Debug("Client closed."); } From 99e7e267cfc49c48adfe292406c17f449dcc924a Mon Sep 17 00:00:00 2001 From: Apolixit Date: Tue, 9 Jul 2024 12:50:23 +0200 Subject: [PATCH 2/6] Change background method --- Substrate.NetApi.TestNode/ClientTests.cs | 6 ++++ Substrate.NetApi/SubstrateClient.cs | 37 ++++++++++-------------- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/Substrate.NetApi.TestNode/ClientTests.cs b/Substrate.NetApi.TestNode/ClientTests.cs index c4bef23..1f64780 100644 --- a/Substrate.NetApi.TestNode/ClientTests.cs +++ b/Substrate.NetApi.TestNode/ClientTests.cs @@ -21,6 +21,12 @@ public void Setup() _client = new SubstrateClient(new Uri("ws://rpc-parachain.bajun.network"), ChargeTransactionPayment.Default()); } + [TearDown] + public async Task TeardownAsync() + { + await _client.CloseAsync(); + } + [Test] public async Task Connect_ShouldConnectSuccessfullyAsync() { diff --git a/Substrate.NetApi/SubstrateClient.cs b/Substrate.NetApi/SubstrateClient.cs index 19910e3..69debc5 100644 --- a/Substrate.NetApi/SubstrateClient.cs +++ b/Substrate.NetApi/SubstrateClient.cs @@ -289,30 +289,23 @@ public async Task ConnectAsync(bool useMetaData, bool standardSubstrate, Cancell } } - _receivedTask = Task.Run(CheckStateAsync); - //_jsonRpc.TraceSource.Switch.Level = SourceLevels.All; - } - - /// - /// Continuously checks the state of the WebSocket connection in an asynchronous loop. - /// - private async Task CheckStateAsync() { - var buffer = new byte[1024 * 4]; - - try { - while(_socket.State == WebSocketState.Open) { - var result = await _socket.ReceiveAsync(new ArraySegment(buffer), _connectTokenSource.Token); - - if(result.MessageType == WebSocketMessageType.Close) { - await _socket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); - - OnConnectionLost(); + // Start a background task to monitor the WebSocket connection + _ = Task.Run(async () => + { + while (IsConnected) + { + await Task.Delay(1000); // Adjust the delay as needed + + if (!IsConnected) + { + // Raise the ConnectionLost event on a separate thread + ThreadPool.QueueUserWorkItem(state => + { + OnConnectionLost(); + }); } } - } catch(WebSocketException) { - // Maybe check : ex.WebSocketErrorCode - OnConnectionLost(); - } + }); } /// From 36585162b3eb802910d1b04236f8268050df6e49 Mon Sep 17 00:00:00 2001 From: Apolixit Date: Tue, 9 Jul 2024 13:00:08 +0200 Subject: [PATCH 3/6] Light adjustments --- Substrate.NetApi/SubstrateClient.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Substrate.NetApi/SubstrateClient.cs b/Substrate.NetApi/SubstrateClient.cs index 69debc5..2233910 100644 --- a/Substrate.NetApi/SubstrateClient.cs +++ b/Substrate.NetApi/SubstrateClient.cs @@ -55,9 +55,9 @@ public class SubstrateClient : IDisposable private ClientWebSocket _socket; /// - /// Task to received connection state + /// The "ping" to check the connection status /// - private Task _receivedTask; + private int _connectionCheckDelay = 500; /// /// The connexion lost event trigger when the websocket change state to disconnected @@ -294,7 +294,7 @@ public async Task ConnectAsync(bool useMetaData, bool standardSubstrate, Cancell { while (IsConnected) { - await Task.Delay(1000); // Adjust the delay as needed + await Task.Delay(_connectionCheckDelay); if (!IsConnected) { From 66150d4935cfcfe8c8a362950e8c6cda0676bdf3 Mon Sep 17 00:00:00 2001 From: Apolixit Date: Tue, 16 Jul 2024 23:02:43 +0200 Subject: [PATCH 4/6] Bind OnConnectionLost on RPC Disconnected event + Add EraEnd --- Substrate.NetApi.Test/Extrinsic/EraTest.cs | 17 +++++++++-- Substrate.NetApi/Model/Extrinsics/Era.cs | 7 +++++ Substrate.NetApi/SubstrateClient.cs | 33 ++++++++-------------- 3 files changed, 33 insertions(+), 24 deletions(-) diff --git a/Substrate.NetApi.Test/Extrinsic/EraTest.cs b/Substrate.NetApi.Test/Extrinsic/EraTest.cs index 9b8f31d..0464b24 100644 --- a/Substrate.NetApi.Test/Extrinsic/EraTest.cs +++ b/Substrate.NetApi.Test/Extrinsic/EraTest.cs @@ -39,10 +39,21 @@ public void EraEncodeDecodeTest() } [Test] - public void EraBeginTest() + [TestCase(64u, 49u, 1587u, 1585u)] + [TestCase(64u, 45u, 21604404u, 21604397u)] + public void EraBeginTest(ulong period, ulong phase, ulong currentBlock, ulong expectedBlock) { - var era = new Era(64, 49, false); - Assert.AreEqual(1585, era.EraStart(1587)); + var era = new Era(period, phase, false); + Assert.AreEqual(expectedBlock, era.EraStart(currentBlock)); + } + + [Test] + [TestCase(64u, 49u, 1587u, 1649u)] + [TestCase(64u, 45u, 21604404u, 21604461u)] + public void EraEndTest(ulong period, ulong phase, ulong currentBlock, ulong expectedBlock) + { + var era = new Era(period, phase, false); + Assert.AreEqual(expectedBlock, era.EraEnd(currentBlock)); } [Test] diff --git a/Substrate.NetApi/Model/Extrinsics/Era.cs b/Substrate.NetApi/Model/Extrinsics/Era.cs index d80ff82..f38e4bf 100644 --- a/Substrate.NetApi/Model/Extrinsics/Era.cs +++ b/Substrate.NetApi/Model/Extrinsics/Era.cs @@ -31,6 +31,13 @@ public class Era : IEncodable /// public ulong EraStart(ulong currentBlockNumber) => IsImmortal ? 0 : (Math.Max(currentBlockNumber, Phase) - Phase) / Period * Period + Phase; + /// + /// Era End + /// + /// + /// + public ulong EraEnd(ulong currentBlockNumber) => EraStart(currentBlockNumber) + Period; + /// /// Initializes a new instance of the class. /// diff --git a/Substrate.NetApi/SubstrateClient.cs b/Substrate.NetApi/SubstrateClient.cs index 2233910..8cf7951 100644 --- a/Substrate.NetApi/SubstrateClient.cs +++ b/Substrate.NetApi/SubstrateClient.cs @@ -233,8 +233,9 @@ public async Task ConnectAsync(bool useMetaData, bool standardSubstrate, Cancell OnConnectionSet(); linkedTokenSource.Dispose(); - //_connectTokenSource.Dispose(); - //_connectTokenSource = null; + _connectTokenSource.Dispose(); + _connectTokenSource = null; + Logger.Debug("Connected to Websocket."); var formatter = new JsonMessageFormatter(); @@ -252,6 +253,13 @@ public async Task ConnectAsync(bool useMetaData, bool standardSubstrate, Cancell _jsonRpc = new JsonRpc(new WebSocketMessageHandler(_socket, formatter)); _jsonRpc.TraceSource.Listeners.Add(new SerilogTraceListener.SerilogTraceListener()); _jsonRpc.TraceSource.Switch.Level = SourceLevels.Warning; + + _jsonRpc.Disconnected += (sender, args) => + { + Logger.Debug("Disconnected from websocket."); + OnConnectionLost(); + }; + _jsonRpc.AddLocalRpcTarget(Listener, new JsonRpcTargetOptions { AllowNonPublicInvocation = false }); _jsonRpc.StartListening(); Logger.Debug("Listening to websocket."); @@ -288,24 +296,6 @@ public async Task ConnectAsync(bool useMetaData, bool standardSubstrate, Cancell Logger.Warning(ex, "Could not deserialize properties on connect."); } } - - // Start a background task to monitor the WebSocket connection - _ = Task.Run(async () => - { - while (IsConnected) - { - await Task.Delay(_connectionCheckDelay); - - if (!IsConnected) - { - // Raise the ConnectionLost event on a separate thread - ThreadPool.QueueUserWorkItem(state => - { - OnConnectionLost(); - }); - } - } - }); } /// @@ -496,6 +486,8 @@ public async Task CloseAsync() /// An asynchronous result. public async Task CloseAsync(CancellationToken token) { + _connectTokenSource?.Cancel(); + await Task.Run(async () => { // cancel remaining request tokens @@ -505,7 +497,6 @@ await Task.Run(async () => if (_socket != null && _socket.State == WebSocketState.Open) { await _socket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); - _connectTokenSource?.Cancel(); _jsonRpc?.Dispose(); Logger.Debug("Client closed."); } From 604048e346ac0990cb970c1dd75ed3a10bc77cb0 Mon Sep 17 00:00:00 2001 From: Apolixit Date: Sat, 14 Sep 2024 12:17:36 +0200 Subject: [PATCH 5/6] Add manually disconnect + Minor fix post merge --- Substrate.NetApi.TestNode/ClientTests.cs | 22 +++++++++++ Substrate.NetApi/SubstrateClient.cs | 48 +++++++++++++++++++++++- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/Substrate.NetApi.TestNode/ClientTests.cs b/Substrate.NetApi.TestNode/ClientTests.cs index 1f64780..4bb339a 100644 --- a/Substrate.NetApi.TestNode/ClientTests.cs +++ b/Substrate.NetApi.TestNode/ClientTests.cs @@ -70,5 +70,27 @@ public async Task OnConnectionLost_ShouldThrowDisconnectedEventAsync() await Task.WhenAny(onConnectionLostTriggered.Task, Task.Delay(TimeSpan.FromMinutes(1))); Assert.That(onConnectionLostTriggered.Task.IsCompleted, Is.True); } + + [Test] + public async Task ManuallyDisconnect_ShouldNotTryToReconnectAsync() + { + await _client.ConnectAsync(); + await _client.CloseAsync(); + + Assert.That(_client.IsConnected, Is.False); + } + + [Test] + public async Task Disconnect_ShouldTryToReconnectAsync() + { + var onReconnectedTriggered = new TaskCompletionSource<(bool, int)>(); + _client.OnReconnected += (sender, e) => onReconnectedTriggered.SetResult((true, e)); + + await _client.ConnectAsync(); + await _client._socket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); + + await Task.WhenAny(onReconnectedTriggered.Task, Task.Delay(TimeSpan.FromMinutes(1))); + Assert.That(_client.IsConnected, Is.True); + } } } diff --git a/Substrate.NetApi/SubstrateClient.cs b/Substrate.NetApi/SubstrateClient.cs index 0333514..82745d8 100644 --- a/Substrate.NetApi/SubstrateClient.cs +++ b/Substrate.NetApi/SubstrateClient.cs @@ -23,6 +23,7 @@ using Substrate.NetApi.Model.Types.Metadata.V14; [assembly: InternalsVisibleTo("Substrate.NetApi.Test")] +[assembly: InternalsVisibleTo("Substrate.NetApi.TestNode")] namespace Substrate.NetApi { @@ -63,7 +64,12 @@ public class SubstrateClient : IDisposable private JsonRpc _jsonRpc; /// The socket. - private ClientWebSocket _socket; + internal ClientWebSocket _socket; + + /// + /// Check if the client has been disconnected manually (to avoid auto-reconnect) + /// + private bool _isDisconnectedManually = false; /// /// The "ping" to check the connection status @@ -80,6 +86,12 @@ public class SubstrateClient : IDisposable /// public event EventHandler ConnectionSet; + /// + /// Event triggered when the connection is reconnected + /// + + public event EventHandler OnReconnected; + /// /// Bypass Remote Certificate Validation. Useful when testing with self-signed SSL certificates. /// @@ -198,6 +210,30 @@ public bool SetJsonRPCTraceLevel(SourceLevels sourceLevels) return true; } + /// + /// Raises the event when the connection to the server is lost. + /// + protected virtual void OnConnectionLost() + { + ConnectionLost?.Invoke(this, EventArgs.Empty); + } + + /// + /// Raises the event when the connection to the server is set. + /// + protected virtual void OnConnectionSet() + { + ConnectionSet?.Invoke(this, EventArgs.Empty); + } + + /// + /// Raises the event when reconnected + /// + protected virtual void OnReconnectedSet(int nbTry) + { + OnReconnected?.Invoke(this, nbTry); + } + /// /// Asynchronously connects to the node. /// @@ -350,6 +386,13 @@ public async Task ConnectAsync(bool useMetaData, bool standardSubstrate, int max private void OnJsonRpcDisconnected(object sender, JsonRpcDisconnectedEventArgs e) { Logger.Error(e.Exception, $"JsonRpc disconnected: {e.Reason}"); + OnConnectionLost(); + + if(_isDisconnectedManually) + { + _isDisconnectedManually = false; + return; + } // Attempt to reconnect asynchronously _ = Task.Run(async () => @@ -385,6 +428,8 @@ await ConnectAsync( ); Logger.Information("Reconnected successfully."); + + OnReconnectedSet(retry); } catch (Exception ex) { @@ -579,6 +624,7 @@ public async Task CloseAsync() public async Task CloseAsync(CancellationToken token) { _connectTokenSource?.Cancel(); + _isDisconnectedManually = true; await Task.Run(async () => { From 6f197fdfd7cd2f7dbba9772f6db32923569dcc24 Mon Sep 17 00:00:00 2001 From: Apolixit Date: Sat, 14 Sep 2024 13:01:23 +0200 Subject: [PATCH 6/6] Remove uncessary boolean and check if jsonRpc is already dispose to avoid reconnect --- Substrate.NetApi/SubstrateClient.cs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/Substrate.NetApi/SubstrateClient.cs b/Substrate.NetApi/SubstrateClient.cs index 82745d8..12b60bd 100644 --- a/Substrate.NetApi/SubstrateClient.cs +++ b/Substrate.NetApi/SubstrateClient.cs @@ -66,11 +66,6 @@ public class SubstrateClient : IDisposable /// The socket. internal ClientWebSocket _socket; - /// - /// Check if the client has been disconnected manually (to avoid auto-reconnect) - /// - private bool _isDisconnectedManually = false; - /// /// The "ping" to check the connection status /// @@ -388,11 +383,7 @@ private void OnJsonRpcDisconnected(object sender, JsonRpcDisconnectedEventArgs e Logger.Error(e.Exception, $"JsonRpc disconnected: {e.Reason}"); OnConnectionLost(); - if(_isDisconnectedManually) - { - _isDisconnectedManually = false; - return; - } + if (_jsonRpc == null || _jsonRpc.IsDisposed) return; // Attempt to reconnect asynchronously _ = Task.Run(async () => @@ -624,7 +615,6 @@ public async Task CloseAsync() public async Task CloseAsync(CancellationToken token) { _connectTokenSource?.Cancel(); - _isDisconnectedManually = true; await Task.Run(async () => {