Skip to content

Commit

Permalink
Feature/v4 (#35)
Browse files Browse the repository at this point in the history
* v4.0:
- timeouts - change miliseconds to TimeSpan
- last chance - disabling
- stream fake message
- StartOrFail, ReconnectOrFail - fail fast approach, throws exception on connection error

* ReconnectionHappened and DisconnectionHappened - extend response, add additional info about close status and caused exception

* DisconnectionHappened stream - enable canceling reconnection
  • Loading branch information
Marfusios authored Dec 5, 2019
1 parent 691a693 commit 06c9d90
Show file tree
Hide file tree
Showing 20 changed files with 835 additions and 152 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ deploy:
provider: script
skip_cleanup: true
script:
- cd src/Websocket.Client && dotnet pack /p:PackageVersion=3.2.$TRAVIS_BUILD_NUMBER -c Release && cd bin/Release && dotnet nuget push **/*.3.2.$TRAVIS_BUILD_NUMBER.nupkg -k $NUGET_API_KEY -s https://api.nuget.org/v3/index.json
- cd src/Websocket.Client && dotnet pack /p:PackageVersion=4.0.$TRAVIS_BUILD_NUMBER -c Release && cd bin/Release && dotnet nuget push **/*.4.0.$TRAVIS_BUILD_NUMBER.nupkg -k $NUGET_API_KEY -s https://api.nuget.org/v3/index.json
on:
branch: master
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2019 Mariusz Kotas
Copyright (c) 2020 Mariusz Kotas

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
2 changes: 2 additions & 0 deletions Websocket.Client.sln.DotSettings
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=Bitmex/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Fakely/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=releaser/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=subprotocol/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=testnet/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
41 changes: 34 additions & 7 deletions src/Websocket.Client/IWebsocketClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Net.WebSockets;
using System.Text;
using System.Threading.Tasks;
using Websocket.Client.Models;

namespace Websocket.Client
{
Expand All @@ -22,24 +23,26 @@ public interface IWebsocketClient : IDisposable
/// <summary>
/// Stream for reconnection event (triggered after the new connection)
/// </summary>
IObservable<ReconnectionType> ReconnectionHappened { get; }
IObservable<ReconnectionInfo> ReconnectionHappened { get; }

/// <summary>
/// Stream for disconnection event (triggered after the connection was lost)
/// </summary>
IObservable<DisconnectionType> DisconnectionHappened { get; }
IObservable<DisconnectionInfo> DisconnectionHappened { get; }

/// <summary>
/// Time range in ms, how long to wait before reconnecting if no message comes from server.
/// Default 60000 ms (1 minute)
/// Set null to disable this feature.
/// Default: 1 minute.
/// </summary>
int ReconnectTimeoutMs { get; set; }
TimeSpan? ReconnectTimeout { get; set; }

/// <summary>
/// Time range in ms, how long to wait before reconnecting if last reconnection failed.
/// Default 60000 ms (1 minute)
/// Set null to disable this feature.
/// Default: 1 minute.
/// </summary>
int ErrorReconnectTimeoutMs { get; set; }
TimeSpan? ErrorReconnectTimeout { get; set; }

/// <summary>
/// Get or set the name of the current websocket client instance.
Expand Down Expand Up @@ -75,10 +78,19 @@ public interface IWebsocketClient : IDisposable
Encoding MessageEncoding { get; set; }

/// <summary>
/// Start listening to the websocket stream on the background thread
/// Start listening to the websocket stream on the background thread.
/// In case of connection error it doesn't throw an exception.
/// Only streams a message via 'DisconnectionHappened' and logs it.
/// </summary>
Task Start();

/// <summary>
/// Start listening to the websocket stream on the background thread.
/// In case of connection error it throws an exception.
/// Fail fast approach.
/// </summary>
Task StartOrFail();

/// <summary>
/// Stop/close websocket connection with custom close code.
/// Method could throw exceptions.
Expand Down Expand Up @@ -121,7 +133,22 @@ public interface IWebsocketClient : IDisposable
/// <summary>
/// Force reconnection.
/// Closes current websocket stream and perform a new connection to the server.
/// In case of connection error it doesn't throw an exception, but tries to reconnect indefinitely.
/// </summary>
Task Reconnect();

/// <summary>
/// Force reconnection.
/// Closes current websocket stream and perform a new connection to the server.
/// In case of connection error it throws an exception and doesn't perform any other reconnection try.
/// </summary>
Task ReconnectOrFail();

/// <summary>
/// Stream/publish fake message (via 'MessageReceived' observable).
/// Use for testing purposes to simulate a server message.
/// </summary>
/// <param name="message">Message to be stream</param>
void StreamFakeMessage(ResponseMessage message);
}
}
64 changes: 64 additions & 0 deletions src/Websocket.Client/Models/DisconnectionInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System;
using System.Net.WebSockets;

// ReSharper disable once CheckNamespace
namespace Websocket.Client
{
/// <summary>
/// Info about happened disconnection
/// </summary>
public class DisconnectionInfo
{
/// <inheritdoc />
public DisconnectionInfo(DisconnectionType type, WebSocketCloseStatus? closeStatus,
string closeStatusDescription, string subProtocol, Exception exception)
{
Type = type;
CloseStatus = closeStatus;
CloseStatusDescription = closeStatusDescription;
SubProtocol = subProtocol;
Exception = exception;
}

/// <summary>
/// Disconnection reason
/// </summary>
public DisconnectionType Type { get; }

/// <summary>
/// Indicates the reason why the remote endpoint initiated the close handshake
/// </summary>
public WebSocketCloseStatus? CloseStatus { get; }

/// <summary>
/// Allows the remote endpoint to describe the reason whe the connection was closed
/// </summary>
public string CloseStatusDescription { get; }

/// <summary>
/// The subprotocol that was negotiated during the opening handshake
/// </summary>
public string SubProtocol { get; }

/// <summary>
/// Exception that cause disconnection, can be null
/// </summary>
public Exception Exception { get; }


/// <summary>
/// Set to true if you want to cancel ongoing reconnection
/// </summary>
public bool CancelReconnection { get; set; }


/// <summary>
/// Simple factory method
/// </summary>
public static DisconnectionInfo Create(DisconnectionType type, WebSocket client, Exception exception)
{
return new DisconnectionInfo(type, client?.CloseStatus, client?.CloseStatusDescription,
client?.SubProtocol, exception);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
namespace Websocket.Client
// ReSharper disable once CheckNamespace
namespace Websocket.Client
{
/// <summary>
/// Type that specify happened disconnection
Expand Down
30 changes: 30 additions & 0 deletions src/Websocket.Client/Models/ReconnectionInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System;

// ReSharper disable once CheckNamespace
namespace Websocket.Client.Models
{
/// <summary>
/// Info about happened reconnection
/// </summary>
public class ReconnectionInfo
{
/// <inheritdoc />
public ReconnectionInfo(ReconnectionType type)
{
Type = type;
}

/// <summary>
/// Reconnection reason
/// </summary>
public ReconnectionType Type { get; }

/// <summary>
/// Simple factory method
/// </summary>
public static ReconnectionInfo Create(ReconnectionType type)
{
return new ReconnectionInfo(type);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
namespace Websocket.Client
// ReSharper disable once CheckNamespace
namespace Websocket.Client
{
/// <summary>
/// Type that specify happened reconnection
Expand Down
10 changes: 5 additions & 5 deletions src/Websocket.Client/Websocket.Client.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<PackageId>Websocket.Client</PackageId>
<Version>3.2.0</Version>
<Version>4.0.0</Version>
<Authors>Mariusz Kotas</Authors>
<Description>Client for websocket API with built-in reconnection and error handling</Description>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageReleaseNotes>Release of version 3.0</PackageReleaseNotes>
<Copyright>Copyright 2019 Mariusz Kotas. All rights reserved.</Copyright>
<PackageReleaseNotes>Release of version 4.0</PackageReleaseNotes>
<Copyright>Copyright 2020 Mariusz Kotas. All rights reserved.</Copyright>
<PackageTags>websockets websocket client</PackageTags>
<PackageLicenseUrl>https://github.com/Marfusios/websocket-client/blob/master/LICENSE</PackageLicenseUrl>
<PackageProjectUrl>https://github.com/Marfusios/websocket-client</PackageProjectUrl>
Expand All @@ -17,8 +17,8 @@
<RepositoryType>Git</RepositoryType>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<AssemblyVersion>3.2.0.0</AssemblyVersion>
<FileVersion>3.2.0.0</FileVersion>
<AssemblyVersion>4.0.0.0</AssemblyVersion>
<FileVersion>4.0.0.0</FileVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
54 changes: 38 additions & 16 deletions src/Websocket.Client/WebsocketClient.Reconnecting.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Websocket.Client.Logging;
Expand All @@ -13,8 +11,24 @@ public partial class WebsocketClient
/// <summary>
/// Force reconnection.
/// Closes current websocket stream and perform a new connection to the server.
/// In case of connection error it doesn't throw an exception, but tries to reconnect indefinitely.
/// </summary>
public async Task Reconnect()
public Task Reconnect()
{
return ReconnectInternal(false);
}

/// <summary>
/// Force reconnection.
/// Closes current websocket stream and perform a new connection to the server.
/// In case of connection error it throws an exception and doesn't perform any other reconnection try.
/// </summary>
public Task ReconnectOrFail()
{
return ReconnectInternal(true);
}

private async Task ReconnectInternal(bool failFast)
{
if (!IsStarted)
{
Expand All @@ -24,39 +38,47 @@ public async Task Reconnect()

try
{
await ReconnectSynchronized(ReconnectionType.ByUser).ConfigureAwait(false);

await ReconnectSynchronized(ReconnectionType.ByUser, failFast, null).ConfigureAwait(false);
}
finally
{
_reconnecting = false;
}
}


private async Task ReconnectSynchronized(ReconnectionType type)
private async Task ReconnectSynchronized(ReconnectionType type, bool failFast, Exception causedException)
{
using (await _locker.LockAsync())
{
await Reconnect(type);
await Reconnect(type, failFast, causedException);
}
}

private async Task Reconnect(ReconnectionType type)
private async Task Reconnect(ReconnectionType type, bool failFast, Exception causedException)
{
IsRunning = false;
if (_disposing)
return;

_reconnecting = true;
if (type != ReconnectionType.Error)
_disconnectedSubject.OnNext(TranslateTypeToDisconnection(type));

var disType = TranslateTypeToDisconnection(type);
var disInfo = DisconnectionInfo.Create(disType, _client, causedException);
if (type != ReconnectionType.Error)
{
_disconnectedSubject.OnNext(disInfo);
if (disInfo.CancelReconnection)
{
// reconnection canceled by user, do nothing
Logger.Info(L($"Reconnecting canceled by user, exiting."));
}
}

_cancellation.Cancel();
_client?.Abort();
_client?.Dispose();

if (!IsReconnectionEnabled)
if (!IsReconnectionEnabled || disInfo.CancelReconnection)
{
// reconnection disabled, do nothing
IsStarted = false;
Expand All @@ -66,7 +88,7 @@ private async Task Reconnect(ReconnectionType type)

Logger.Debug(L("Reconnecting..."));
_cancellation = new CancellationTokenSource();
await StartClient(_url, _cancellation.Token, type).ConfigureAwait(false);
await StartClient(_url, _cancellation.Token, type, failFast).ConfigureAwait(false);
_reconnecting = false;
}

Expand All @@ -84,22 +106,22 @@ private void DeactivateLastChance()

private void LastChance(object state)
{
if (!IsReconnectionEnabled)
if (!IsReconnectionEnabled || ReconnectTimeout == null)
{
// reconnection disabled, do nothing
DeactivateLastChance();
return;
}

var timeoutMs = Math.Abs(ReconnectTimeoutMs);
var timeoutMs = Math.Abs(ReconnectTimeout.Value.TotalMilliseconds);
var diffMs = Math.Abs(DateTime.UtcNow.Subtract(_lastReceivedMsg).TotalMilliseconds);
if (diffMs > timeoutMs)
{
Logger.Debug(L($"Last message received more than {timeoutMs:F} ms ago. Hard restart.."));

DeactivateLastChance();
#pragma warning disable 4014
ReconnectSynchronized(ReconnectionType.NoMessageReceived);
ReconnectSynchronized(ReconnectionType.NoMessageReceived, false, null);
#pragma warning restore 4014
}
}
Expand Down
12 changes: 12 additions & 0 deletions src/Websocket.Client/WebsocketClient.Sending.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,18 @@ public Task SendInstant(byte[] message)
return SendInternalSynchronized(message);
}

/// <summary>
/// Stream/publish fake message (via 'MessageReceived' observable).
/// Use for testing purposes to simulate a server message.
/// </summary>
/// <param name="message">Message to be stream</param>
public void StreamFakeMessage(ResponseMessage message)
{
Validations.Validations.ValidateInput(message, nameof(message));

_messageReceivedSubject.OnNext(message);
}


private async Task SendTextFromQueue()
{
Expand Down
Loading

0 comments on commit 06c9d90

Please sign in to comment.