-
-
Notifications
You must be signed in to change notification settings - Fork 66
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Better timeout management and error handling of results (#672)
* Added better handling of timeouts and background error logging * Doc better why we just log warning on timeout * Added test and fix comment * Remove from DI * Remove the interface * Fix tests * Fix tests
- Loading branch information
1 parent
ae025b9
commit 1263652
Showing
15 changed files
with
260 additions
and
46 deletions.
There are no files selected for viewing
85 changes: 85 additions & 0 deletions
85
src/Client/NetDaemon.HassClient.Tests/HelperTest/ResultMessageHandlerTests.cs
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,85 @@ | ||
namespace NetDaemon.HassClient.Tests.HelperTest; | ||
|
||
public class ResultMessageHandlerTests | ||
{ | ||
private readonly Mock<ILogger<ResultMessageHandler>> _loggerMock = new(); | ||
private readonly ResultMessageHandler _resultMessageHandler; | ||
|
||
public ResultMessageHandlerTests() | ||
{ | ||
_resultMessageHandler = new ResultMessageHandler(_loggerMock.Object); | ||
} | ||
|
||
[Fact] | ||
public async Task TestTaskCompleteWithoutLog() | ||
{ | ||
var task = SomeSuccessfulResult(); | ||
_resultMessageHandler.HandleResult(task, new CommandMessage {Type = "test"}); | ||
await _resultMessageHandler.DisposeAsync().ConfigureAwait(false); | ||
task.IsCompletedSuccessfully.Should().BeTrue(); | ||
_loggerMock.Verify( | ||
x => x.Log( | ||
It.IsAny<LogLevel>(), | ||
It.IsAny<EventId>(), | ||
It.Is<It.IsAnyType>((_, __) => true), | ||
It.IsAny<Exception>(), | ||
It.Is<Func<It.IsAnyType, Exception, string>>((_, _) => true)!), Times.Never()); | ||
} | ||
|
||
|
||
[Fact] | ||
public async Task TestTaskCompleteWithErrorResultShouldLogWarning() | ||
{ | ||
var task = SomeUnSuccessfulResult(); | ||
_resultMessageHandler.HandleResult(task, new CommandMessage {Type = "test"}); | ||
await _resultMessageHandler.DisposeAsync().ConfigureAwait(false); | ||
task.IsCompletedSuccessfully.Should().BeTrue(); | ||
_loggerMock.VerifyWarningWasCalled("Failed command (test) error: (null). Sent command is CommandMessage { Type = test, Id = 0 }"); | ||
} | ||
|
||
[Fact] | ||
public async Task TestTaskCompleteWithExceptionShouldLogError() | ||
{ | ||
var task = SomeUnSuccessfulResultThrowsException(); | ||
_resultMessageHandler.HandleResult(task, new CommandMessage {Type = "test"}); | ||
await _resultMessageHandler.DisposeAsync().ConfigureAwait(false); | ||
task.IsCompletedSuccessfully.Should().BeFalse(); | ||
task.IsFaulted.Should().BeTrue(); | ||
_loggerMock.VerifyErrorWasCalled("Exception waiting for result message Sent command is CommandMessage { Type = test, Id = 0 }"); | ||
} | ||
|
||
[Fact] | ||
public async Task TestTaskCompleteWithTimeoutShouldLogWarning() | ||
{ | ||
_resultMessageHandler.WaitForResultTimeout = 1; | ||
|
||
var task = SomeSuccessfulResult(); | ||
_resultMessageHandler.HandleResult(task, new CommandMessage {Type = "test"}); | ||
await _resultMessageHandler.DisposeAsync().ConfigureAwait(false); | ||
task.IsCompletedSuccessfully.Should().BeTrue(); | ||
_loggerMock.VerifyWarningWasCalled("Command (test) did not get response in timely fashion. Sent command is CommandMessage { Type = test, Id = 0 }"); | ||
} | ||
|
||
|
||
|
||
private async Task<HassMessage> SomeSuccessfulResult() | ||
{ | ||
// Simulate som time | ||
await Task.Delay(100); | ||
return new HassMessage {Success = true}; | ||
} | ||
|
||
private async Task<HassMessage> SomeUnSuccessfulResult() | ||
{ | ||
// Simulate som time | ||
await Task.Delay(100); | ||
return new HassMessage {Success = false}; | ||
} | ||
|
||
private async Task<HassMessage> SomeUnSuccessfulResultThrowsException() | ||
{ | ||
// Simulate som time | ||
await Task.Delay(100); | ||
throw new InvalidOperationException("Ohh noooo!"); | ||
} | ||
} |
32 changes: 32 additions & 0 deletions
32
src/Client/NetDaemon.HassClient.Tests/Helpers/LoggerMockExtensions.cs
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,32 @@ | ||
namespace NetDaemon.HassClient.Tests.Helpers; | ||
|
||
public static class LoggerMockExtensions | ||
{ | ||
public static Mock<ILogger<T>> VerifyWarningWasCalled<T>(this Mock<ILogger<T>> logger, string expectedMessage) | ||
{ | ||
Func<object, Type, bool> state = (v, t) => String.Compare(v.ToString()!, expectedMessage, StringComparison.Ordinal) == 0; | ||
|
||
logger.Verify( | ||
x => x.Log( | ||
It.Is<LogLevel>(l => l == LogLevel.Warning), | ||
It.IsAny<EventId>(), | ||
It.Is<It.IsAnyType>((v, t) => state(v, t)), | ||
It.IsAny<Exception>(), | ||
It.Is<Func<It.IsAnyType, Exception, string>>((v, t) => true)!)); | ||
return logger; | ||
} | ||
|
||
public static Mock<ILogger<T>> VerifyErrorWasCalled<T>(this Mock<ILogger<T>> logger, string expectedMessage) | ||
{ | ||
Func<object, Type, bool> state = (v, t) => String.Compare(v.ToString()!, expectedMessage, StringComparison.Ordinal) == 0; | ||
|
||
logger.Verify( | ||
x => x.Log( | ||
It.Is<LogLevel>(l => l == LogLevel.Error), | ||
It.IsAny<EventId>(), | ||
It.Is<It.IsAnyType>((v, t) => state(v, t)), | ||
It.IsAny<Exception>(), | ||
It.Is<Func<It.IsAnyType, Exception, string>>((v, t) => true)!)); | ||
return logger; | ||
} | ||
} |
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
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
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
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
74 changes: 74 additions & 0 deletions
74
src/Client/NetDaemon.HassClient/Internal/Helpers/ResultMessageHandler.cs
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,74 @@ | ||
using System.Collections.Concurrent; | ||
|
||
namespace NetDaemon.Client.Internal.Helpers; | ||
|
||
internal class ResultMessageHandler : IAsyncDisposable | ||
{ | ||
internal int WaitForResultTimeout = 20000; | ||
private readonly CancellationTokenSource _tokenSource = new(); | ||
private readonly ConcurrentDictionary<Task<HassMessage>, object?> _backgroundTasks = new(); | ||
private readonly ILogger _logger; | ||
|
||
public ResultMessageHandler(ILogger logger) | ||
{ | ||
_logger = logger; | ||
} | ||
|
||
public void HandleResult(Task<HassMessage> returnMessageTask, CommandMessage originalCommand) | ||
{ | ||
TrackBackgroundTask(returnMessageTask, originalCommand); | ||
} | ||
|
||
private void TrackBackgroundTask(Task<HassMessage> task, CommandMessage command) | ||
{ | ||
_backgroundTasks.TryAdd(task, null); | ||
|
||
[SuppressMessage("", "CA1031")] | ||
async Task Wrap() | ||
{ | ||
try | ||
{ | ||
var awaitedTask = await Task.WhenAny(task, Task.Delay(WaitForResultTimeout, _tokenSource.Token)).ConfigureAwait(false); | ||
|
||
if (awaitedTask != task) | ||
{ | ||
// We have a timeout | ||
_logger.LogWarning( | ||
"Command ({CommandType}) did not get response in timely fashion. Sent command is {CommandMessage}", | ||
command.Type, command); | ||
} | ||
// We wait for the task even if there was a timeout so we make sure | ||
// we catch the original error | ||
var result = await task.ConfigureAwait(false); | ||
if (!result.Success ?? false) | ||
{ | ||
_logger.LogWarning( | ||
"Failed command ({CommandType}) error: {ErrorResult}. Sent command is {CommandMessage}", | ||
command.Type, result.Error, command); | ||
} | ||
} | ||
catch (Exception e) | ||
{ | ||
_logger.LogError(e, "Exception waiting for result message Sent command is {CommandMessage}", command); | ||
} | ||
finally | ||
{ | ||
_backgroundTasks.TryRemove(task, out var _); | ||
} | ||
} | ||
|
||
// We do not handle task here cause exceptions | ||
// are handled in the Wrap local functions and | ||
// all tasks should be cancelable | ||
_ = Wrap(); | ||
} | ||
|
||
public async ValueTask DisposeAsync() | ||
{ | ||
// Wait for the tasks to complete max 5 seconds | ||
if (!_backgroundTasks.IsEmpty) | ||
{ | ||
await Task.WhenAny( Task.WhenAll(_backgroundTasks.Keys), Task.Delay(TimeSpan.FromSeconds(5))).ConfigureAwait(false); | ||
} | ||
} | ||
} |
Oops, something went wrong.