Skip to content

Commit 4aede33

Browse files
authored
Better handling of faulty unloading of appa (#961)
* handle unloading of faulty apps better and prohibit apps instancing if previous unloading fails * refactor * test for slow dispose
1 parent e7b8487 commit 4aede33

File tree

7 files changed

+127
-24
lines changed

7 files changed

+127
-24
lines changed

src/AppModel/NetDaemon.AppModel.Tests/AppModelTests.cs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,37 @@ public async Task TestGetApplicationsLocal()
3737
var loadApps = await TestHelpers.GetLocalApplicationsFromYamlConfigPath("Fixtures/Local").ConfigureAwait(false);
3838

3939
// CHECK
40-
loadApps.Should().HaveCount(6);
40+
loadApps.Should().HaveCount(7);
4141

4242
// check the application instance is init ok
4343
var application = (Application) loadApps.First(n => n.Id == "LocalApps.MyAppLocalApp");
4444
var instance = (MyAppLocalApp?) application.ApplicationContext?.Instance;
4545
instance!.Settings.AString.Should().Be("Hello world!");
4646
}
4747

48+
[Fact]
49+
public async Task TestSlowDisposableAppShouldLogWarning()
50+
{
51+
// ARRANGE
52+
var loggerMock = new Mock<ILogger<Application>>();
53+
54+
// ACT
55+
var loadApps = await TestHelpers.GetLocalApplicationsFromYamlConfigPath("Fixtures/Local", loggerMock.Object).ConfigureAwait(false);
56+
57+
// CHECK
58+
// check the application instance is init ok
59+
var application = (Application) loadApps.First(n => n.Id == "LocalApps.SlowDisposableApp");
60+
await application.DisposeAsync().ConfigureAwait(false);
61+
62+
loggerMock.Verify(
63+
x => x.Log(
64+
LogLevel.Error,
65+
It.IsAny<EventId>(),
66+
It.Is<It.IsAnyType>((_, __) => true),
67+
It.IsAny<Exception>(),
68+
It.Is<Func<It.IsAnyType, Exception?, string>>((_, _) => true)), Times.Once);
69+
}
70+
4871
[Fact]
4972
public async Task TestGetApplicationsLocalWithDisabled()
5073
{
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
2+
namespace LocalApps;
3+
4+
5+
[NetDaemonApp]
6+
public class SlowDisposableApp : IDisposable
7+
{
8+
public SlowDisposableApp()
9+
{
10+
}
11+
12+
public void Dispose()
13+
{
14+
Thread.Sleep(3500);
15+
}
16+
}
17+

src/AppModel/NetDaemon.AppModel.Tests/Helpers/TestHelpers.cs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
using System.Reflection;
22
using Microsoft.Extensions.Hosting;
3+
using Microsoft.Extensions.Logging;
4+
using NetDaemon.AppModel.Internal;
35

46
namespace NetDaemon.AppModel.Tests.Helpers;
57

68
public static class TestHelpers
79
{
10+
811
internal static async Task<IReadOnlyCollection<IApplication>> GetLocalApplicationsFromYamlConfigPath(string path)
912
{
1013
var builder = Host.CreateDefaultBuilder()
@@ -25,6 +28,27 @@ internal static async Task<IReadOnlyCollection<IApplication>> GetLocalApplicatio
2528
return appModelContext.Applications;
2629
}
2730

31+
internal static async Task<IReadOnlyCollection<IApplication>> GetLocalApplicationsFromYamlConfigPath(string path, ILogger<Application> logger)
32+
{
33+
var builder = Host.CreateDefaultBuilder()
34+
.ConfigureServices((_, services) =>
35+
{
36+
// get apps from test project
37+
services.AddAppsFromAssembly(Assembly.GetExecutingAssembly());
38+
services.AddTransient<ILogger<Application>>(_ => logger);
39+
})
40+
.ConfigureAppConfiguration((_, config) =>
41+
{
42+
config.AddYamlAppConfig(
43+
Path.Combine(AppContext.BaseDirectory,
44+
path));
45+
})
46+
.Build();
47+
var appModel = builder.Services.GetService<IAppModel>();
48+
var appModelContext = await appModel!.LoadNewApplicationContext(CancellationToken.None).ConfigureAwait(false);
49+
return appModelContext.Applications;
50+
}
51+
2852
internal static async Task<IReadOnlyCollection<IApplication>> GetDynamicApplicationsFromYamlConfigPath(string path)
2953
{
3054
var builder = Host.CreateDefaultBuilder()
@@ -46,4 +70,4 @@ internal static async Task<IReadOnlyCollection<IApplication>> GetDynamicApplicat
4670
var appModelContext = await appModel!.LoadNewApplicationContext(CancellationToken.None).ConfigureAwait(false);
4771
return appModelContext.Applications;
4872
}
49-
}
73+
}

src/AppModel/NetDaemon.AppModel/Internal/AppModelContext.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public async Task InitializeAsync(CancellationToken cancellationToken)
2626
{
2727
var factories = _appFactoryProviders.SelectMany(provider => provider.GetAppFactories()).ToList();
2828

29-
var filteredFactories = _focusFilter.FilterFocusApps(factories);
29+
var filteredFactories = _focusFilter.FilterFocusApps(factories);
3030

3131
foreach (var factory in filteredFactories)
3232
{
@@ -44,10 +44,10 @@ public async ValueTask DisposeAsync()
4444
if (_isDisposed) return;
4545
_isDisposed = true;
4646

47-
foreach (var appInstance in _applications)
48-
{
49-
await appInstance.DisposeAsync().ConfigureAwait(false);
50-
}
47+
// Get all tasks for disposing the apps with a timeout
48+
var disposeTasks = _applications.Select(app => app.DisposeAsync().AsTask()).ToList();
49+
50+
await Task.WhenAll(disposeTasks).ConfigureAwait(false);
5151

5252
_applications.Clear();
5353
}

src/AppModel/NetDaemon.AppModel/Internal/Application.cs

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,20 +58,43 @@ public async Task SetStateAsync(ApplicationState state)
5858
}
5959
}
6060

61+
/// <summary>
62+
/// Unloads the application and log any errors.If application unload takes longer than 3 seconds it will log a warning and return.
63+
/// </summary>
64+
private async Task UnloadContextAsync()
65+
{
66+
if (ApplicationContext is not null)
67+
{
68+
try
69+
{
70+
await ApplicationContext.DisposeAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(3)).ConfigureAwait(false);
71+
}
72+
catch (Exception e)
73+
{
74+
if (e is TimeoutException)
75+
_logger.LogError("Unloading the app '{App}' takes longer than expected, please check for blocking code", Id);
76+
else
77+
_logger.LogError(e, "Unloading the app '{App}' failed", Id);
78+
79+
ApplicationContext = null;
80+
return;
81+
}
82+
}
83+
84+
ApplicationContext = null;
85+
_logger.LogDebug("Successfully unloaded app {Id}", Id);
86+
}
87+
6188
public async ValueTask DisposeAsync()
6289
{
63-
if (ApplicationContext is not null) await ApplicationContext.DisposeAsync().ConfigureAwait(false);
90+
await UnloadContextAsync().ConfigureAwait(false);
6491
}
6592

6693
private async Task UnloadApplication(ApplicationState state)
6794
{
68-
if (ApplicationContext is not null)
69-
{
70-
await ApplicationContext.DisposeAsync().ConfigureAwait(false);
71-
ApplicationContext = null;
72-
_logger.LogInformation("Successfully unloaded app {Id}", Id);
73-
await SaveStateIfStateManagerExistAsync(state).ConfigureAwait(false);
74-
}
95+
await UnloadContextAsync().ConfigureAwait(false);
96+
97+
await SaveStateIfStateManagerExistAsync(state).ConfigureAwait(false);
7598
}
7699

77100
private async Task LoadApplication(ApplicationState state)

src/Runtime/NetDaemon.Runtime/Internal/NetDaemonRuntime.cs

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,14 @@ private async Task OnHomeAssistantClientConnected(
8585
CancellationToken cancelToken)
8686
{
8787
_logger.LogInformation("Successfully connected to Home Assistant");
88+
89+
if (_applicationModelContext is not null)
90+
{
91+
// Something wrong with unloading and disposing apps on restart of HA, we need to prevent apps loading multiple times
92+
_logger.LogWarning("Applications were not successfully disposed during restart, skippin loading apps again");
93+
return;
94+
}
95+
8896
IsConnected = true;
8997

9098
await _cacheManager.InitializeAsync(cancelToken).ConfigureAwait(false);
@@ -108,13 +116,13 @@ private async Task LoadNewAppContextAsync(IHomeAssistantConnection haConnection,
108116
Path.GetFullPath(_locationSettings.Value.ApplicationConfigurationFolder));
109117
else
110118
_logger.LogDebug("Loading applications with no configuration folder");
111-
119+
112120
_applicationModelContext = await appModel.LoadNewApplicationContext(CancellationToken.None).ConfigureAwait(false);
113121

114122
// Handle state change for apps if registered
115123
var appStateHandler = _serviceProvider.GetService<IHandleHomeAssistantAppStateUpdates>();
116124
if (appStateHandler == null) return;
117-
125+
118126
await appStateHandler.InitializeAsync(haConnection, _applicationModelContext);
119127
}
120128
catch (Exception e)
@@ -144,7 +152,14 @@ private async Task OnHomeAssistantClientDisconnected(DisconnectReason reason)
144152
reasonString, TimeoutInSeconds);
145153
}
146154

147-
await DisposeApplicationsAsync().ConfigureAwait(false);
155+
try
156+
{
157+
await DisposeApplicationsAsync().ConfigureAwait(false);
158+
}
159+
catch (Exception e)
160+
{
161+
_logger.LogError(e, "Error disposing applications");
162+
}
148163
IsConnected = false;
149164
}
150165

@@ -153,7 +168,7 @@ private async Task DisposeApplicationsAsync()
153168
if (_applicationModelContext is not null)
154169
{
155170
await _applicationModelContext.DisposeAsync();
156-
171+
157172
_applicationModelContext = null;
158173
}
159174
}
@@ -163,7 +178,7 @@ public async ValueTask DisposeAsync()
163178
{
164179
if (_isDisposed) return;
165180
_isDisposed = true;
166-
181+
167182
await DisposeApplicationsAsync().ConfigureAwait(false);
168183
_runnerCancelationSource?.Cancel();
169184
try
@@ -172,4 +187,4 @@ public async ValueTask DisposeAsync()
172187
}
173188
catch (OperationCanceledException) { }
174189
}
175-
}
190+
}

src/debug/DebugHost/apps/HelloApp/HelloApp.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ public HelloApp(IHaContext ha, ILogger<HelloApp> logger)
2323
// ha?.CallService("notify", "persistent_notification", data: new { message = "Notify me", title = "Hello world!" });
2424
}
2525

26-
public ValueTask DisposeAsync()
26+
public async ValueTask DisposeAsync()
2727
{
28+
await Task.Delay(5000);
2829
_logger.LogInformation("disposed app");
29-
return ValueTask.CompletedTask;
30+
//return ValueTask.CompletedTask;
3031
}
31-
}
32+
}

0 commit comments

Comments
 (0)