Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stability fixes #1131

Merged
merged 30 commits into from
Jul 6, 2024
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
f729a28
Make the disposing of application context thread safe
helto4real Jul 4, 2024
3d2cdde
Fix the example app using get calendar events
helto4real Jul 4, 2024
116c321
Make the handling of cancellation more roboust to avoid the source to…
helto4real Jul 4, 2024
4b12ac0
added tests
helto4real Jul 4, 2024
1a39300
Fixed the throw instead
helto4real Jul 4, 2024
1418f0c
Ignore and tracelog the operation cancelled exception in background task
helto4real Jul 4, 2024
d3982a5
revert throwifcanceled #1
helto4real Jul 5, 2024
1df6c93
revert throwif cancelled #2
helto4real Jul 5, 2024
adb1913
Add the Flush feature on BackgroundTaskTracker
helto4real Jul 5, 2024
daedfa7
Fix the scheduler to be more robust and not call actions on disposed …
helto4real Jul 5, 2024
3a63fb7
Fix the code comment using interlocked instead
helto4real Jul 5, 2024
c4ce7ca
Fix warning and missing setting the disposed flag
helto4real Jul 5, 2024
6ac57d6
add test
helto4real Jul 5, 2024
dda52d0
Added context tests
helto4real Jul 5, 2024
ca0ab18
Added even more tests
helto4real Jul 5, 2024
61df6d5
add tests
helto4real Jul 5, 2024
72ba56d
fix test
helto4real Jul 5, 2024
1a01805
remove the last of throwifcancelled
helto4real Jul 5, 2024
98e002f
Add one more test
helto4real Jul 5, 2024
7af8423
renaming
helto4real Jul 5, 2024
3a597e6
Fix comments and from discussion
helto4real Jul 5, 2024
fc1296c
removed timing using TaskSource
helto4real Jul 5, 2024
9919c8e
using interlocked on more places
helto4real Jul 5, 2024
967ac9c
removed the disposable timer
helto4real Jul 5, 2024
8b7ad1b
fix last interlocked case
helto4real Jul 5, 2024
28119f4
forgott the test
helto4real Jul 5, 2024
1d39a59
revert back to volatile bools
helto4real Jul 6, 2024
78542ef
Added more checks for disposed and throws if so
helto4real Jul 6, 2024
13ef23a
added test for multople dispose
helto4real Jul 6, 2024
53bafb5
fix from comment
helto4real Jul 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/AppModel/NetDaemon.AppModel.Tests/AppModelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,21 @@ public async Task TestGetApplicationsLocalWithDisposable()
app!.DisposeIsCalled.Should().BeTrue();
}

[Fact]
public async Task TestApplicationCanDisposeMultipleTimesWithoutExceptions()
{
// ARRANGE
// ACT
var loadApps = await TestHelpers.GetLocalApplicationsFromYamlConfigPath("Fixtures/Local");

// CHECK

// check the application instance is init ok
var application = loadApps.First(n => n.Id == "LocalApps.MyAppLocalAppWithDispose");
await application.DisposeAsync().ConfigureAwait(false);
await application.DisposeAsync().ConfigureAwait(false);
}

[Fact]
public async Task TestGetApplicationsLocalWithAsyncDisposable()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using NetDaemon.AppModel.Internal;
using NetDaemon.AppModel.Internal.AppFactories;

namespace NetDaemon.AppModel.Tests.Context;

public class ApplicationContextTests
{
[Fact]
public async Task TestApplicationContextIsDisposedMultipleTimesNotThrowsException()
{
var serviceProvider = new ServiceCollection().BuildServiceProvider();
var appFactory = Mock.Of<IAppFactory>();
var applicationContext = new ApplicationContext(serviceProvider, appFactory);

await applicationContext.DisposeAsync();
await applicationContext.DisposeAsync();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ internal class AppModelContext : IAppModelContext
private readonly IServiceProvider _provider;
private readonly FocusFilter _focusFilter;
private ILogger<AppModelContext> _logger;
private bool _isDisposed;
private volatile bool _isDisposed;

public AppModelContext(IEnumerable<IAppFactoryProvider> appFactoryProviders, IServiceProvider provider, FocusFilter focusFilter, ILogger<AppModelContext> logger)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ internal sealed class ApplicationContext : IAsyncDisposable
{
private readonly CancellationTokenSource _cancelTokenSource = new();
private readonly IServiceScope? _serviceScope;
private bool _isDisposed;
private volatile bool _isDisposed;

public ApplicationContext(IServiceProvider serviceProvider, IAppFactory appFactory)
{
Expand Down Expand Up @@ -34,9 +34,7 @@ public async Task InitializeAsync()

public async ValueTask DisposeAsync()
{
// prevent multiple Disposes because the Service Scope will also dispose this
if (_isDisposed) return;

_isDisposed = true;

if (!_cancelTokenSource.IsCancellationRequested)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
namespace NetDaemon.HassClient.Tests.HomeAssistantClientTest;

public record FakeCommand : CommandMessage {}

public class HomeAssistantConnectionTests
{
private static IHomeAssistantConnection GetDefaultHomeAssistantConnection()
{
var pipeline = new TransportPipelineMock();
pipeline.Setup(n => n.WebSocketState).Returns(WebSocketState.Open);
var apiManagerMock = new Mock<IHomeAssistantApiManager>();
var loggerMock = new Mock<ILogger<IHomeAssistantConnection>>();
return new HomeAssistantConnection(loggerMock.Object, pipeline.Object, apiManagerMock.Object);
}

[Fact]
public async Task UsingDisposedConnectionWhenSendCommandShouldThrowException()
{
var homeAssistantConnection = GetDefaultHomeAssistantConnection();
await homeAssistantConnection!.DisposeAsync();

Func<Task> act = async () =>
{
await homeAssistantConnection!.SendCommandAsync(new FakeCommand(), CancellationToken.None);
};

await act.Should().ThrowAsync<ObjectDisposedException>();
}

[Fact]
public async Task UsingDisposedConnectionWhenGetApiCommandShouldThrowException()
{
var homeAssistantConnection = GetDefaultHomeAssistantConnection();
await homeAssistantConnection!.DisposeAsync();

Func<Task> act = async () =>
{
await homeAssistantConnection!.GetApiCallAsync<string>("test", CancellationToken.None);
};

await act.Should().ThrowAsync<ObjectDisposedException>();
}

[Fact]
public async Task UsingDisposedConnectionWhenPostApiShouldThrowException()
{
var homeAssistantConnection = GetDefaultHomeAssistantConnection();
await homeAssistantConnection!.DisposeAsync();

Func<Task> act = async () =>
{
await homeAssistantConnection!.PostApiCallAsync<string>("test", CancellationToken.None, null);
};

await act.Should().ThrowAsync<ObjectDisposedException>();
}

[Fact]
public async Task HomeAssistantConnectionDisposedMultipleTimesShouldNotThrow()
{
var homeAssistantConnection = GetDefaultHomeAssistantConnection();
await homeAssistantConnection!.DisposeAsync();
await homeAssistantConnection!.DisposeAsync();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ internal class HomeAssistantConnection : IHomeAssistantConnection, IHomeAssistan
{
#region -- private declarations -

private volatile bool _isDisposed;

private readonly ILogger<IHomeAssistantConnection> _logger;
private readonly IWebSocketClientTransportPipeline _transportPipeline;
private readonly IHomeAssistantApiManager _apiManager;
Expand Down Expand Up @@ -132,6 +134,9 @@ public async Task SendCommandAsync<T>(T command, CancellationToken cancelToken)

public async ValueTask DisposeAsync()
{
if (_isDisposed) return;
_isDisposed = true;

try
{
await CloseAsync().ConfigureAwait(false);
Expand Down Expand Up @@ -159,18 +164,22 @@ await Task.WhenAny(

public Task<T?> GetApiCallAsync<T>(string apiPath, CancellationToken cancelToken)
{
ObjectDisposedException.ThrowIf(_isDisposed, nameof(HomeAssistantConnection));
return _apiManager.GetApiCallAsync<T>(apiPath, cancelToken);
}

public Task<T?> PostApiCallAsync<T>(string apiPath, CancellationToken cancelToken, object? data = null)
{
ObjectDisposedException.ThrowIf(_isDisposed, nameof(HomeAssistantConnection));
return _apiManager.PostApiCallAsync<T>(apiPath, cancelToken, data);
}

public IObservable<HassMessage> OnHassMessage => _hassMessageSubject;

private async Task<Task<HassMessage>> SendCommandAsyncInternal<T>(T command, CancellationToken cancelToken) where T : CommandMessage
{
ObjectDisposedException.ThrowIf(_isDisposed, nameof(HomeAssistantConnection));

// The semaphore can fail to be taken in rare cases so we need
// to keep this out of the try/finally block so it will not be released
await _messageIdSemaphore.WaitAsync(cancelToken).ConfigureAwait(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
internal class MessageSubscriber : IMessageSubscriber, IDisposable
{
private readonly SemaphoreSlim _subscriptionSetupLock = new SemaphoreSlim(1);
private bool _isDisposed;
private volatile bool _isDisposed;
private bool _subscriptionIsSetup;
private readonly IAssuredMqttConnection _assuredMqttConnection;
private readonly ILogger<MessageSubscriber> _logger;
Expand Down Expand Up @@ -121,17 +121,16 @@

public void Dispose()
{
if (!_isDisposed)
{
_isDisposed = true;
foreach (var observer in _subscribers)
{
_logger.LogTrace("Disposing {Topic} subscription", observer.Key);
observer.Value.Value.OnCompleted();
observer.Value.Value.Dispose();
}
if (_isDisposed) return;
_isDisposed = true;

Check warning on line 125 in src/Extensions/NetDaemon.Extensions.MqttEntityManager/MessageSubscriber.cs

View check run for this annotation

Codecov / codecov/patch

src/Extensions/NetDaemon.Extensions.MqttEntityManager/MessageSubscriber.cs#L125

Added line #L125 was not covered by tests

_subscriptionSetupLock.Dispose();
foreach (var observer in _subscribers)
{
_logger.LogTrace("Disposing {Topic} subscription", observer.Key);
observer.Value.Value.OnCompleted();
observer.Value.Value.Dispose();

Check warning on line 131 in src/Extensions/NetDaemon.Extensions.MqttEntityManager/MessageSubscriber.cs

View check run for this annotation

Codecov / codecov/patch

src/Extensions/NetDaemon.Extensions.MqttEntityManager/MessageSubscriber.cs#L129-L131

Added lines #L129 - L131 were not covered by tests
}

_subscriptionSetupLock.Dispose();

Check warning on line 134 in src/Extensions/NetDaemon.Extensions.MqttEntityManager/MessageSubscriber.cs

View check run for this annotation

Codecov / codecov/patch

src/Extensions/NetDaemon.Extensions.MqttEntityManager/MessageSubscriber.cs#L134

Added line #L134 was not covered by tests
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,47 @@ public void _SchedulePeriodicStopsAfterDisposeOfDisposableScheduler()
}


[Fact]
public void PeriodicSchedulerShouldNotCallActionIfItIsDisposedDuringSchedule()
{
var (inner, disposableScheduler) = CreateScheduler();

int called = 0;
using var _ = disposableScheduler.SchedulePeriodic(TimeSpan.FromMinutes(1), () => called++);

// Dispose before the time moves forward and trigger a schedule
disposableScheduler.Dispose();

inner.AdvanceBy(TimeSpan.FromMinutes(1).Ticks);
called.Should().Be(0);
}

[Fact]
public void PeriodicSchedulerShouldNotCallActionIfItIsDisposedBeforeScheduled()
{
var (inner, disposableScheduler) = CreateScheduler();

int called = 0;
// Dispose before the we call schedule
disposableScheduler.Dispose();
using var _ = disposableScheduler.SchedulePeriodic(TimeSpan.FromMinutes(1), () => called++);
inner.AdvanceBy(TimeSpan.FromMinutes(1).Ticks);
called.Should().Be(0);
}

[Fact]
public void SchedulerShouldNotCallActionIfItIsDisposedBeforeScheduled()
{
var (inner, disposableScheduler) = CreateScheduler();

int called = 0;
// Dispose before the we call schedule
disposableScheduler.Dispose();
using var _ = disposableScheduler.Schedule(() => called++);
inner.AdvanceBy(TimeSpan.FromMinutes(1).Ticks);
called.Should().Be(0);
}

[Fact]
public void SchedulePeriodicStopsAfterDisposeOfDisposableScheduler()
{
Expand Down

This file was deleted.

Loading
Loading