This repository has been archived by the owner on Mar 28, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Workaround for aborted websocket issue added;
- Loading branch information
Showing
3 changed files
with
188 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
using System; | ||
using System.Data; | ||
using System.Reflection; | ||
|
||
namespace ReflectionExtensions { | ||
public static class ReflectionExts { | ||
static void ArgumentNotNull(object instance, string name) { | ||
if ( instance == null ) { | ||
throw new ArgumentNullException(name); | ||
} | ||
} | ||
|
||
static void ValueNotNull(object instance, string message) { | ||
if ( instance == null ) { | ||
throw new InvalidExpressionException(message); | ||
} | ||
} | ||
|
||
public static object GetPrivateField(this object instance, string name) { | ||
ArgumentNotNull(instance, nameof(instance)); | ||
var type = instance.GetType(); | ||
var fieldInfo = type.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic); | ||
ValueNotNull(fieldInfo, $"Can't find private instance field '{name}' on object of type {type.FullName}"); | ||
var fieldValue = fieldInfo.GetValue(instance); | ||
return fieldValue; | ||
} | ||
|
||
public static T GetPrivateField<T>(this object instance, string name) { | ||
var rawFieldValue = GetPrivateField(instance, name); | ||
var fieldValue = (T)rawFieldValue; | ||
return fieldValue; | ||
} | ||
|
||
public static object InvokePrivateMethod(this object instance, string name, params object[] args) { | ||
ArgumentNotNull(instance, nameof(instance)); | ||
var type = instance.GetType(); | ||
var methodInfo = type.GetMethod(name, BindingFlags.Instance | BindingFlags.NonPublic); | ||
ValueNotNull(methodInfo, $"Can't find private instance method '{name}' on object of type {type.FullName}"); | ||
var result = methodInfo.Invoke(instance, args); | ||
return result; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
using System; | ||
using System.Net.WebSockets; | ||
using System.Threading.Tasks; | ||
using Microsoft.Extensions.Logging; | ||
using ReflectionExtensions; | ||
using SlackBotNet; | ||
|
||
namespace SlackBotNetExtensions { | ||
/// <summary> | ||
/// Workaround to handle invalid websocket state inside SlackBotNet.SlackBot | ||
/// It can happens randomly, especially when client running for long time | ||
/// When it happens, bot refused to handle any messages and logs shown such messages: | ||
/// WARN [SlackBotNet.SlackBot] [0] Not pinging because the socket is not open. Current state is: Aborted | ||
/// Possible cause of this behaviour is SlackRtmDriver.cs: | ||
/// ConnectAsync(...) | ||
/// { | ||
/// Observable | ||
/// .Timer(TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5)) | ||
/// .Subscribe(async _ => | ||
/// { | ||
/// ... | ||
/// if (this.websocket.State != WebSocketState.Open) // This condition prevent driver from further work | ||
/// { | ||
/// logger.LogWarning($"Not pinging because the socket is not open. Current state is: {this.websocket.State}"); | ||
/// return; | ||
/// } | ||
/// ... | ||
/// } | ||
/// ... | ||
/// } | ||
/// Method to restart websocket connection is ReconnectRtmAsync, but in called twice below this condition and it can't be reached | ||
/// To workaround this situation without modifying library sources, we need to call it manually using reflection, | ||
/// when websocket is invalid state | ||
/// Layout of required classes: | ||
/// SlackBot.cs: | ||
/// public class SlackBot | ||
/// { | ||
/// ... | ||
/// private readonly IMessageBus messageBus; | ||
/// private readonly ILogger`SlackBot` logger; | ||
/// private IDriver driver; // SlackRtmDriver | ||
/// ... | ||
/// } | ||
/// SlackRtmDriver.cs: | ||
/// class SlackRtmDriver | ||
/// { | ||
/// ... | ||
/// private ClientWebSocket websocket; | ||
/// ... | ||
/// private async Task`SlackBotState` ReconnectRtmAsync(IMessageBus bus, ILogger logger) // result ignored | ||
/// ... | ||
/// } | ||
/// </summary> | ||
public class WebsocketResurrector { | ||
static double _defaultInterval = 30; | ||
static Func<WebSocketState, bool> _defaultSelector = state => state == WebSocketState.Aborted; | ||
|
||
readonly SlackBot _bot; | ||
readonly ILogger _logger; | ||
readonly double _checkInterval; | ||
readonly Func<WebSocketState, bool> _invalidStateSelector; | ||
|
||
bool _stopped = false; | ||
|
||
public WebsocketResurrector( | ||
SlackBot bot, ILoggerFactory loggerFactory, | ||
double checkInterval, Func<WebSocketState, bool> invalidStateSelector | ||
) { | ||
_bot = bot; | ||
_logger = loggerFactory?.CreateLogger<WebsocketResurrector>(); | ||
_checkInterval = checkInterval; | ||
_invalidStateSelector = invalidStateSelector; | ||
} | ||
|
||
public WebsocketResurrector(SlackBot bot, ILoggerFactory loggerFactory) | ||
: this(bot, loggerFactory, _defaultInterval, _defaultSelector) {} | ||
|
||
/// <summary> | ||
/// Start tracking websocket state | ||
/// </summary> | ||
public void Start() { | ||
Task.Run(async () => { | ||
while ( !_stopped ) { | ||
await Task.Delay(TimeSpan.FromSeconds(_checkInterval)); | ||
await TryResurrectWebsocket(); | ||
} | ||
}); | ||
} | ||
|
||
/// <summary> | ||
/// Stop tracking websocket state | ||
/// </summary> | ||
public void Stop() { | ||
_stopped = true; | ||
} | ||
|
||
async Task TryResurrectWebsocket() { | ||
var driver = _bot.GetPrivateField("driver"); | ||
var client = GetClient(driver); | ||
var oldState = client.State; | ||
if ( IsNeedToReconnect(oldState) ) { | ||
_logger?.LogWarning($"Websocket state is invalid: {oldState}, try to resurrect it"); | ||
await ResurrectWebsocket(driver); | ||
} | ||
} | ||
|
||
ClientWebSocket GetClient(object driver) => driver.GetPrivateField<ClientWebSocket>("websocket"); | ||
|
||
bool IsNeedToReconnect(WebSocketState state) => _invalidStateSelector(state); | ||
|
||
async Task ResurrectWebsocket(object driver) { | ||
var messageBus = _bot.GetPrivateField("messageBus"); | ||
var logger = _bot.GetPrivateField<ILogger<SlackBot>>("logger"); | ||
var reconnectRtmAsyncTask = driver.InvokePrivateMethod("ReconnectRtmAsync", messageBus, logger); | ||
switch ( reconnectRtmAsyncTask ) { | ||
case Task task: { | ||
await task; | ||
var client = GetClient(driver); | ||
_logger?.LogWarning($"Reconnect completed, new state is: {client.State}"); | ||
break; | ||
} | ||
|
||
case null: { | ||
_logger?.LogError($"Unexpected null result of reconnect method"); | ||
break; | ||
} | ||
|
||
default: { | ||
_logger?.LogError($"Invalid result of reconnect method ({reconnectRtmAsyncTask})"); | ||
break; | ||
} | ||
} | ||
} | ||
} | ||
} |