mirror of
https://github.com/lidarr/lidarr.git
synced 2025-08-20 13:33:34 -07:00
Fixed: Duplicate notifications for failed health checks
Mock debouncer for testing (cherry picked from commit c0e54773e213f526a5046fa46aa7b57532471128) (cherry picked from commit bb7b2808e2f70389157408809ec47cc8860b4938)
This commit is contained in:
parent
20e1a6e41c
commit
fb21dd0f19
7 changed files with 164 additions and 101 deletions
17
src/NzbDrone.Common/TPL/DebounceManager.cs
Normal file
17
src/NzbDrone.Common/TPL/DebounceManager.cs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace NzbDrone.Common.TPL
|
||||||
|
{
|
||||||
|
public interface IDebounceManager
|
||||||
|
{
|
||||||
|
Debouncer CreateDebouncer(Action action, TimeSpan debounceDuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DebounceManager : IDebounceManager
|
||||||
|
{
|
||||||
|
public Debouncer CreateDebouncer(Action action, TimeSpan debounceDuration)
|
||||||
|
{
|
||||||
|
return new Debouncer(action, debounceDuration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,12 +4,12 @@ namespace NzbDrone.Common.TPL
|
||||||
{
|
{
|
||||||
public class Debouncer
|
public class Debouncer
|
||||||
{
|
{
|
||||||
private readonly Action _action;
|
protected readonly Action _action;
|
||||||
private readonly System.Timers.Timer _timer;
|
protected readonly System.Timers.Timer _timer;
|
||||||
private readonly bool _executeRestartsTimer;
|
protected readonly bool _executeRestartsTimer;
|
||||||
|
|
||||||
private volatile int _paused;
|
protected volatile int _paused;
|
||||||
private volatile bool _triggered;
|
protected volatile bool _triggered;
|
||||||
|
|
||||||
public Debouncer(Action action, TimeSpan debounceDuration, bool executeRestartsTimer = false)
|
public Debouncer(Action action, TimeSpan debounceDuration, bool executeRestartsTimer = false)
|
||||||
{
|
{
|
||||||
|
@ -29,11 +29,12 @@ namespace NzbDrone.Common.TPL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Execute()
|
public virtual void Execute()
|
||||||
{
|
{
|
||||||
lock (_timer)
|
lock (_timer)
|
||||||
{
|
{
|
||||||
_triggered = true;
|
_triggered = true;
|
||||||
|
|
||||||
if (_executeRestartsTimer)
|
if (_executeRestartsTimer)
|
||||||
{
|
{
|
||||||
_timer.Stop();
|
_timer.Stop();
|
||||||
|
@ -46,7 +47,7 @@ namespace NzbDrone.Common.TPL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Pause()
|
public virtual void Pause()
|
||||||
{
|
{
|
||||||
lock (_timer)
|
lock (_timer)
|
||||||
{
|
{
|
||||||
|
@ -55,7 +56,7 @@ namespace NzbDrone.Common.TPL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Resume()
|
public virtual void Resume()
|
||||||
{
|
{
|
||||||
lock (_timer)
|
lock (_timer)
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
|
using Moq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NzbDrone.Common.Cache;
|
using NzbDrone.Common.Cache;
|
||||||
using NzbDrone.Common.Messaging;
|
using NzbDrone.Common.Messaging;
|
||||||
|
using NzbDrone.Common.TPL;
|
||||||
using NzbDrone.Core.HealthCheck;
|
using NzbDrone.Core.HealthCheck;
|
||||||
using NzbDrone.Core.Test.Framework;
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
using NzbDrone.Test.Common;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Test.HealthCheck
|
namespace NzbDrone.Core.Test.HealthCheck
|
||||||
{
|
{
|
||||||
|
@ -19,10 +23,10 @@ namespace NzbDrone.Core.Test.HealthCheck
|
||||||
|
|
||||||
Mocker.SetConstant<IEnumerable<IProvideHealthCheck>>(new[] { _healthCheck });
|
Mocker.SetConstant<IEnumerable<IProvideHealthCheck>>(new[] { _healthCheck });
|
||||||
Mocker.SetConstant<ICacheManager>(Mocker.Resolve<CacheManager>());
|
Mocker.SetConstant<ICacheManager>(Mocker.Resolve<CacheManager>());
|
||||||
|
Mocker.SetConstant<IDebounceManager>(Mocker.Resolve<DebounceManager>());
|
||||||
|
|
||||||
Mocker.GetMock<IServerSideNotificationService>()
|
Mocker.GetMock<IDebounceManager>().Setup(s => s.CreateDebouncer(It.IsAny<Action>(), It.IsAny<TimeSpan>()))
|
||||||
.Setup(v => v.GetServerChecks())
|
.Returns<Action, TimeSpan>((a, t) => new MockDebouncer(a, t));
|
||||||
.Returns(new List<Core.HealthCheck.HealthCheck>());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NLog;
|
|
||||||
using NzbDrone.Common.Cache;
|
using NzbDrone.Common.Cache;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Common.Messaging;
|
using NzbDrone.Common.Messaging;
|
||||||
using NzbDrone.Common.Reflection;
|
using NzbDrone.Common.Reflection;
|
||||||
|
using NzbDrone.Common.TPL;
|
||||||
using NzbDrone.Core.Lifecycle;
|
using NzbDrone.Core.Lifecycle;
|
||||||
using NzbDrone.Core.Messaging.Commands;
|
using NzbDrone.Core.Messaging.Commands;
|
||||||
using NzbDrone.Core.Messaging.Events;
|
using NzbDrone.Core.Messaging.Events;
|
||||||
|
@ -27,30 +27,27 @@ namespace NzbDrone.Core.HealthCheck
|
||||||
private readonly IProvideHealthCheck[] _startupHealthChecks;
|
private readonly IProvideHealthCheck[] _startupHealthChecks;
|
||||||
private readonly IProvideHealthCheck[] _scheduledHealthChecks;
|
private readonly IProvideHealthCheck[] _scheduledHealthChecks;
|
||||||
private readonly Dictionary<Type, IEventDrivenHealthCheck[]> _eventDrivenHealthChecks;
|
private readonly Dictionary<Type, IEventDrivenHealthCheck[]> _eventDrivenHealthChecks;
|
||||||
private readonly IServerSideNotificationService _serverSideNotificationService;
|
|
||||||
private readonly IEventAggregator _eventAggregator;
|
private readonly IEventAggregator _eventAggregator;
|
||||||
private readonly ICacheManager _cacheManager;
|
|
||||||
private readonly Logger _logger;
|
|
||||||
|
|
||||||
private readonly ICached<HealthCheck> _healthCheckResults;
|
private readonly ICached<HealthCheck> _healthCheckResults;
|
||||||
|
private readonly HashSet<IProvideHealthCheck> _pendingHealthChecks;
|
||||||
|
private readonly Debouncer _debounce;
|
||||||
|
|
||||||
private bool _hasRunHealthChecksAfterGracePeriod;
|
private bool _hasRunHealthChecksAfterGracePeriod;
|
||||||
private bool _isRunningHealthChecksAfterGracePeriod;
|
private bool _isRunningHealthChecksAfterGracePeriod;
|
||||||
|
|
||||||
public HealthCheckService(IEnumerable<IProvideHealthCheck> healthChecks,
|
public HealthCheckService(IEnumerable<IProvideHealthCheck> healthChecks,
|
||||||
IServerSideNotificationService serverSideNotificationService,
|
|
||||||
IEventAggregator eventAggregator,
|
IEventAggregator eventAggregator,
|
||||||
ICacheManager cacheManager,
|
ICacheManager cacheManager,
|
||||||
IRuntimeInfo runtimeInfo,
|
IDebounceManager debounceManager,
|
||||||
Logger logger)
|
IRuntimeInfo runtimeInfo)
|
||||||
{
|
{
|
||||||
_healthChecks = healthChecks.ToArray();
|
_healthChecks = healthChecks.ToArray();
|
||||||
_serverSideNotificationService = serverSideNotificationService;
|
|
||||||
_eventAggregator = eventAggregator;
|
_eventAggregator = eventAggregator;
|
||||||
_cacheManager = cacheManager;
|
|
||||||
_logger = logger;
|
|
||||||
|
|
||||||
_healthCheckResults = _cacheManager.GetCache<HealthCheck>(GetType());
|
_healthCheckResults = cacheManager.GetCache<HealthCheck>(GetType());
|
||||||
|
_pendingHealthChecks = new HashSet<IProvideHealthCheck>();
|
||||||
|
_debounce = debounceManager.CreateDebouncer(ProcessHealthChecks, TimeSpan.FromSeconds(5));
|
||||||
|
|
||||||
_startupHealthChecks = _healthChecks.Where(v => v.CheckOnStartup).ToArray();
|
_startupHealthChecks = _healthChecks.Where(v => v.CheckOnStartup).ToArray();
|
||||||
_scheduledHealthChecks = _healthChecks.Where(v => v.CheckOnSchedule).ToArray();
|
_scheduledHealthChecks = _healthChecks.Where(v => v.CheckOnSchedule).ToArray();
|
||||||
|
@ -77,26 +74,22 @@ namespace NzbDrone.Core.HealthCheck
|
||||||
.ToDictionary(g => g.Key, g => g.ToArray());
|
.ToDictionary(g => g.Key, g => g.ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PerformHealthCheck(IProvideHealthCheck[] healthChecks, IEvent message = null, bool performServerChecks = false)
|
private void ProcessHealthChecks()
|
||||||
{
|
{
|
||||||
var results = new List<HealthCheck>();
|
List<IProvideHealthCheck> healthChecks;
|
||||||
|
|
||||||
foreach (var healthCheck in healthChecks)
|
lock (_pendingHealthChecks)
|
||||||
{
|
{
|
||||||
if (healthCheck is IProvideHealthCheckWithMessage && message != null)
|
healthChecks = _pendingHealthChecks.ToList();
|
||||||
{
|
_pendingHealthChecks.Clear();
|
||||||
results.Add(((IProvideHealthCheckWithMessage)healthCheck).Check(message));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
results.Add(healthCheck.Check());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (performServerChecks)
|
_debounce.Pause();
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
results.AddRange(_serverSideNotificationService.GetServerChecks());
|
var results = healthChecks.Select(c => c.Check())
|
||||||
}
|
.ToList();
|
||||||
|
|
||||||
foreach (var result in results)
|
foreach (var result in results)
|
||||||
{
|
{
|
||||||
|
@ -121,30 +114,46 @@ namespace NzbDrone.Core.HealthCheck
|
||||||
_healthCheckResults.Set(result.Source.Name, result);
|
_healthCheckResults.Set(result.Source.Name, result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_debounce.Resume();
|
||||||
|
}
|
||||||
|
|
||||||
_eventAggregator.PublishEvent(new HealthCheckCompleteEvent());
|
_eventAggregator.PublishEvent(new HealthCheckCompleteEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Execute(CheckHealthCommand message)
|
public void Execute(CheckHealthCommand message)
|
||||||
{
|
{
|
||||||
if (message.Trigger == CommandTrigger.Manual)
|
var healthChecks = message.Trigger == CommandTrigger.Manual ? _healthChecks : _scheduledHealthChecks;
|
||||||
|
|
||||||
|
lock (_pendingHealthChecks)
|
||||||
{
|
{
|
||||||
PerformHealthCheck(_healthChecks, null, true);
|
foreach (var healthCheck in healthChecks)
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
PerformHealthCheck(_scheduledHealthChecks, null, true);
|
_pendingHealthChecks.Add(healthCheck);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ProcessHealthChecks();
|
||||||
|
}
|
||||||
|
|
||||||
public void HandleAsync(ApplicationStartedEvent message)
|
public void HandleAsync(ApplicationStartedEvent message)
|
||||||
{
|
{
|
||||||
PerformHealthCheck(_startupHealthChecks, null, true);
|
lock (_pendingHealthChecks)
|
||||||
|
{
|
||||||
|
foreach (var healthCheck in _startupHealthChecks)
|
||||||
|
{
|
||||||
|
_pendingHealthChecks.Add(healthCheck);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ProcessHealthChecks();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void HandleAsync(IEvent message)
|
public void HandleAsync(IEvent message)
|
||||||
{
|
{
|
||||||
if (message is HealthCheckCompleteEvent)
|
if (message is HealthCheckCompleteEvent || message is ApplicationStartedEvent)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -155,7 +164,16 @@ namespace NzbDrone.Core.HealthCheck
|
||||||
{
|
{
|
||||||
_isRunningHealthChecksAfterGracePeriod = true;
|
_isRunningHealthChecksAfterGracePeriod = true;
|
||||||
|
|
||||||
PerformHealthCheck(_startupHealthChecks);
|
lock (_pendingHealthChecks)
|
||||||
|
{
|
||||||
|
foreach (var healthCheck in _startupHealthChecks)
|
||||||
|
{
|
||||||
|
_pendingHealthChecks.Add(healthCheck);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call it directly so it's not debounced and any alerts can be sent.
|
||||||
|
ProcessHealthChecks();
|
||||||
|
|
||||||
// Update after running health checks so new failure notifications aren't sent 2x.
|
// Update after running health checks so new failure notifications aren't sent 2x.
|
||||||
_hasRunHealthChecksAfterGracePeriod = true;
|
_hasRunHealthChecksAfterGracePeriod = true;
|
||||||
|
@ -191,8 +209,12 @@ namespace NzbDrone.Core.HealthCheck
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Add debounce
|
lock (_pendingHealthChecks)
|
||||||
PerformHealthCheck(filteredChecks.ToArray(), message);
|
{
|
||||||
|
filteredChecks.ForEach(h => _pendingHealthChecks.Add(h));
|
||||||
|
}
|
||||||
|
|
||||||
|
_debounce.Execute();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
using NzbDrone.Common.Messaging;
|
|
||||||
|
|
||||||
namespace NzbDrone.Core.HealthCheck
|
namespace NzbDrone.Core.HealthCheck
|
||||||
{
|
{
|
||||||
public interface IProvideHealthCheck
|
public interface IProvideHealthCheck
|
||||||
|
@ -8,9 +6,4 @@ namespace NzbDrone.Core.HealthCheck
|
||||||
bool CheckOnStartup { get; }
|
bool CheckOnStartup { get; }
|
||||||
bool CheckOnSchedule { get; }
|
bool CheckOnSchedule { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IProvideHealthCheckWithMessage : IProvideHealthCheck
|
|
||||||
{
|
|
||||||
HealthCheck Check(IEvent message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,45 +9,43 @@ using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
using NzbDrone.Common.Serializer;
|
using NzbDrone.Common.Serializer;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
|
using NzbDrone.Core.Localization;
|
||||||
|
|
||||||
namespace NzbDrone.Core.HealthCheck
|
namespace NzbDrone.Core.HealthCheck
|
||||||
{
|
{
|
||||||
public interface IServerSideNotificationService
|
public class ServerSideNotificationService : HealthCheckBase
|
||||||
{
|
|
||||||
public List<HealthCheck> GetServerChecks();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ServerSideNotificationService : IServerSideNotificationService
|
|
||||||
{
|
{
|
||||||
private readonly IHttpClient _client;
|
private readonly IHttpClient _client;
|
||||||
private readonly IConfigFileProvider _configFileProvider;
|
private readonly IConfigFileProvider _configFileProvider;
|
||||||
private readonly IHttpRequestBuilderFactory _cloudRequestBuilder;
|
private readonly ILidarrCloudRequestBuilder _cloudRequestBuilder;
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
|
|
||||||
private readonly ICached<List<HealthCheck>> _cache;
|
private readonly ICached<HealthCheck> _cache;
|
||||||
|
|
||||||
public ServerSideNotificationService(IHttpClient client,
|
public ServerSideNotificationService(IHttpClient client,
|
||||||
IConfigFileProvider configFileProvider,
|
|
||||||
ILidarrCloudRequestBuilder cloudRequestBuilder,
|
ILidarrCloudRequestBuilder cloudRequestBuilder,
|
||||||
|
IConfigFileProvider configFileProvider,
|
||||||
|
ILocalizationService localizationService,
|
||||||
ICacheManager cacheManager,
|
ICacheManager cacheManager,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
|
: base(localizationService)
|
||||||
{
|
{
|
||||||
_client = client;
|
_client = client;
|
||||||
|
_cloudRequestBuilder = cloudRequestBuilder;
|
||||||
_configFileProvider = configFileProvider;
|
_configFileProvider = configFileProvider;
|
||||||
_cloudRequestBuilder = cloudRequestBuilder.Services;
|
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
_cache = cacheManager.GetCache<List<HealthCheck>>(GetType());
|
_cache = cacheManager.GetCache<HealthCheck>(GetType());
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<HealthCheck> GetServerChecks()
|
public override HealthCheck Check()
|
||||||
{
|
{
|
||||||
return _cache.Get("ServerChecks", () => RetrieveServerChecks(), TimeSpan.FromHours(2));
|
return _cache.Get("ServerChecks", RetrieveServerChecks, TimeSpan.FromHours(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<HealthCheck> RetrieveServerChecks()
|
private HealthCheck RetrieveServerChecks()
|
||||||
{
|
{
|
||||||
var request = _cloudRequestBuilder.Create()
|
var request = _cloudRequestBuilder.Services.Create()
|
||||||
.Resource("/notification")
|
.Resource("/notification")
|
||||||
.AddQueryParam("version", BuildInfo.Version)
|
.AddQueryParam("version", BuildInfo.Version)
|
||||||
.AddQueryParam("os", OsInfo.Os.ToString().ToLowerInvariant())
|
.AddQueryParam("os", OsInfo.Os.ToString().ToLowerInvariant())
|
||||||
|
@ -55,17 +53,24 @@ namespace NzbDrone.Core.HealthCheck
|
||||||
.AddQueryParam("runtime", "netcore")
|
.AddQueryParam("runtime", "netcore")
|
||||||
.AddQueryParam("branch", _configFileProvider.Branch)
|
.AddQueryParam("branch", _configFileProvider.Branch)
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.Trace("Getting server side health notifications");
|
_logger.Trace("Getting notifications");
|
||||||
|
|
||||||
var response = _client.Execute(request);
|
var response = _client.Execute(request);
|
||||||
var result = Json.Deserialize<List<ServerNotificationResponse>>(response.Content);
|
var result = Json.Deserialize<List<ServerNotificationResponse>>(response.Content);
|
||||||
return result.Select(x => new HealthCheck(GetType(), x.Type, x.Message, x.WikiUrl)).ToList();
|
|
||||||
|
var checks = result.Select(x => new HealthCheck(GetType(), x.Type, x.Message, x.WikiUrl)).ToList();
|
||||||
|
|
||||||
|
// Only one health check is supported, services returns an ordered list, so use the first one
|
||||||
|
return checks.FirstOrDefault() ?? new HealthCheck(GetType());
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.Error(ex, "Failed to retrieve server notifications");
|
_logger.Error(ex, "Failed to retrieve notifications");
|
||||||
return new List<HealthCheck>();
|
|
||||||
|
return new HealthCheck(GetType());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
21
src/NzbDrone.Test.Common/MockDebouncer.cs
Normal file
21
src/NzbDrone.Test.Common/MockDebouncer.cs
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
using System;
|
||||||
|
using NzbDrone.Common.TPL;
|
||||||
|
|
||||||
|
namespace NzbDrone.Test.Common
|
||||||
|
{
|
||||||
|
public class MockDebouncer : Debouncer
|
||||||
|
{
|
||||||
|
public MockDebouncer(Action action, TimeSpan debounceDuration)
|
||||||
|
: base(action, debounceDuration)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Execute()
|
||||||
|
{
|
||||||
|
lock (_timer)
|
||||||
|
{
|
||||||
|
_action();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue