diff --git a/frontend/src/System/Status/Health/Health.js b/frontend/src/System/Status/Health/Health.js
index fedbf9903..57693dcb3 100644
--- a/frontend/src/System/Status/Health/Health.js
+++ b/frontend/src/System/Status/Health/Health.js
@@ -38,6 +38,14 @@ function getInternalLink(source) {
to="/settings/downloadclients"
/>
);
+ case 'NotificationStatusCheck':
+ return (
+
+ );
case 'RootFolderCheck':
return (
+ {
+ private List _notifications = new List();
+ private List _blockedNotifications = new List();
+
+ [SetUp]
+ public void SetUp()
+ {
+ Mocker.GetMock()
+ .Setup(v => v.GetAvailableProviders())
+ .Returns(_notifications);
+
+ Mocker.GetMock()
+ .Setup(v => v.GetBlockedProviders())
+ .Returns(_blockedNotifications);
+
+ Mocker.GetMock()
+ .Setup(s => s.GetLocalizedString(It.IsAny()))
+ .Returns("Some Warning Message");
+ }
+
+ private Mock GivenNotification(int id, double backoffHours, double failureHours)
+ {
+ var mockNotification = new Mock();
+ mockNotification.SetupGet(s => s.Definition).Returns(new NotificationDefinition { Id = id });
+
+ _notifications.Add(mockNotification.Object);
+
+ if (backoffHours != 0.0)
+ {
+ _blockedNotifications.Add(new NotificationStatus
+ {
+ ProviderId = id,
+ InitialFailure = DateTime.UtcNow.AddHours(-failureHours),
+ MostRecentFailure = DateTime.UtcNow.AddHours(-0.1),
+ EscalationLevel = 5,
+ DisabledTill = DateTime.UtcNow.AddHours(backoffHours)
+ });
+ }
+
+ return mockNotification;
+ }
+
+ [Test]
+ public void should_not_return_error_when_no_notifications()
+ {
+ Subject.Check().ShouldBeOk();
+ }
+
+ [Test]
+ public void should_return_warning_if_notification_unavailable()
+ {
+ GivenNotification(1, 10.0, 24.0);
+ GivenNotification(2, 0.0, 0.0);
+
+ Subject.Check().ShouldBeWarning();
+ }
+
+ [Test]
+ public void should_return_error_if_all_notifications_unavailable()
+ {
+ GivenNotification(1, 10.0, 24.0);
+
+ Subject.Check().ShouldBeError();
+ }
+
+ [Test]
+ public void should_return_warning_if_few_notifications_unavailable()
+ {
+ GivenNotification(1, 10.0, 24.0);
+ GivenNotification(2, 10.0, 24.0);
+ GivenNotification(3, 0.0, 0.0);
+
+ Subject.Check().ShouldBeWarning();
+ }
+ }
+}
diff --git a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedNotificationStatusFixture.cs b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedNotificationStatusFixture.cs
new file mode 100644
index 000000000..20e82ff7f
--- /dev/null
+++ b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedNotificationStatusFixture.cs
@@ -0,0 +1,56 @@
+using FizzWare.NBuilder;
+using FluentAssertions;
+using NUnit.Framework;
+using NzbDrone.Core.Housekeeping.Housekeepers;
+using NzbDrone.Core.Notifications;
+using NzbDrone.Core.Notifications.Join;
+using NzbDrone.Core.Test.Framework;
+
+namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
+{
+ [TestFixture]
+ public class CleanupOrphanedNotificationStatusFixture : DbTest
+ {
+ private NotificationDefinition _notification;
+
+ [SetUp]
+ public void Setup()
+ {
+ _notification = Builder.CreateNew()
+ .With(s => s.Settings = new JoinSettings { })
+ .BuildNew();
+ }
+
+ private void GivenNotification()
+ {
+ Db.Insert(_notification);
+ }
+
+ [Test]
+ public void should_delete_orphaned_notificationstatus()
+ {
+ var status = Builder.CreateNew()
+ .With(h => h.ProviderId = _notification.Id)
+ .BuildNew();
+ Db.Insert(status);
+
+ Subject.Clean();
+ AllStoredModels.Should().BeEmpty();
+ }
+
+ [Test]
+ public void should_not_delete_unorphaned_notificationstatus()
+ {
+ GivenNotification();
+
+ var status = Builder.CreateNew()
+ .With(h => h.ProviderId = _notification.Id)
+ .BuildNew();
+ Db.Insert(status);
+
+ Subject.Clean();
+ AllStoredModels.Should().HaveCount(1);
+ AllStoredModels.Should().Contain(h => h.ProviderId == _notification.Id);
+ }
+ }
+}
diff --git a/src/NzbDrone.Core.Test/NotificationTests/NotificationStatusServiceFixture.cs b/src/NzbDrone.Core.Test/NotificationTests/NotificationStatusServiceFixture.cs
new file mode 100644
index 000000000..183246313
--- /dev/null
+++ b/src/NzbDrone.Core.Test/NotificationTests/NotificationStatusServiceFixture.cs
@@ -0,0 +1,161 @@
+using System;
+using System.Linq;
+using FluentAssertions;
+using Moq;
+using NUnit.Framework;
+using NzbDrone.Common.EnvironmentInfo;
+using NzbDrone.Core.Notifications;
+using NzbDrone.Core.Test.Framework;
+
+namespace NzbDrone.Core.Test.NotificationTests
+{
+ public class NotificationStatusServiceFixture : CoreTest
+ {
+ private DateTime _epoch;
+
+ [SetUp]
+ public void SetUp()
+ {
+ _epoch = DateTime.UtcNow;
+
+ Mocker.GetMock()
+ .SetupGet(v => v.StartTime)
+ .Returns(_epoch - TimeSpan.FromHours(1));
+ }
+
+ private NotificationStatus WithStatus(NotificationStatus status)
+ {
+ Mocker.GetMock()
+ .Setup(v => v.FindByProviderId(1))
+ .Returns(status);
+
+ Mocker.GetMock()
+ .Setup(v => v.All())
+ .Returns(new[] { status });
+
+ return status;
+ }
+
+ private void VerifyUpdate()
+ {
+ Mocker.GetMock()
+ .Verify(v => v.Upsert(It.IsAny()), Times.Once());
+ }
+
+ private void VerifyNoUpdate()
+ {
+ Mocker.GetMock()
+ .Verify(v => v.Upsert(It.IsAny()), Times.Never());
+ }
+
+ [Test]
+ public void should_not_consider_blocked_within_5_minutes_since_initial_failure()
+ {
+ WithStatus(new NotificationStatus
+ {
+ InitialFailure = _epoch - TimeSpan.FromMinutes(4),
+ MostRecentFailure = _epoch - TimeSpan.FromSeconds(4),
+ EscalationLevel = 3
+ });
+
+ Subject.RecordFailure(1);
+
+ VerifyUpdate();
+
+ var status = Subject.GetBlockedProviders().FirstOrDefault();
+ status.Should().BeNull();
+ }
+
+ [Test]
+ public void should_consider_blocked_after_5_minutes_since_initial_failure()
+ {
+ WithStatus(new NotificationStatus
+ {
+ InitialFailure = _epoch - TimeSpan.FromMinutes(6),
+ MostRecentFailure = _epoch - TimeSpan.FromSeconds(120),
+ EscalationLevel = 3
+ });
+
+ Subject.RecordFailure(1);
+
+ VerifyUpdate();
+
+ var status = Subject.GetBlockedProviders().FirstOrDefault();
+ status.Should().NotBeNull();
+ }
+
+ [Test]
+ public void should_not_escalate_further_till_after_5_minutes_since_initial_failure()
+ {
+ var origStatus = WithStatus(new NotificationStatus
+ {
+ InitialFailure = _epoch - TimeSpan.FromMinutes(4),
+ MostRecentFailure = _epoch - TimeSpan.FromSeconds(4),
+ EscalationLevel = 3
+ });
+
+ Subject.RecordFailure(1);
+ Subject.RecordFailure(1);
+ Subject.RecordFailure(1);
+ Subject.RecordFailure(1);
+ Subject.RecordFailure(1);
+ Subject.RecordFailure(1);
+ Subject.RecordFailure(1);
+
+ var status = Subject.GetBlockedProviders().FirstOrDefault();
+ status.Should().BeNull();
+
+ origStatus.EscalationLevel.Should().Be(3);
+ }
+
+ [Test]
+ public void should_escalate_further_after_5_minutes_since_initial_failure()
+ {
+ WithStatus(new NotificationStatus
+ {
+ InitialFailure = _epoch - TimeSpan.FromMinutes(6),
+ MostRecentFailure = _epoch - TimeSpan.FromSeconds(120),
+ EscalationLevel = 3
+ });
+
+ Subject.RecordFailure(1);
+ Subject.RecordFailure(1);
+ Subject.RecordFailure(1);
+ Subject.RecordFailure(1);
+ Subject.RecordFailure(1);
+ Subject.RecordFailure(1);
+ Subject.RecordFailure(1);
+
+ var status = Subject.GetBlockedProviders().FirstOrDefault();
+ status.Should().NotBeNull();
+
+ status.EscalationLevel.Should().BeGreaterThan(3);
+ }
+
+ [Test]
+ public void should_not_escalate_beyond_3_hours()
+ {
+ WithStatus(new NotificationStatus
+ {
+ InitialFailure = _epoch - TimeSpan.FromMinutes(6),
+ MostRecentFailure = _epoch - TimeSpan.FromSeconds(120),
+ EscalationLevel = 3
+ });
+
+ Subject.RecordFailure(1);
+ Subject.RecordFailure(1);
+ Subject.RecordFailure(1);
+ Subject.RecordFailure(1);
+ Subject.RecordFailure(1);
+ Subject.RecordFailure(1);
+ Subject.RecordFailure(1);
+ Subject.RecordFailure(1);
+ Subject.RecordFailure(1);
+
+ var status = Subject.GetBlockedProviders().FirstOrDefault();
+ status.Should().NotBeNull();
+ status.DisabledTill.Should().HaveValue();
+ status.DisabledTill.Should().NotBeAfter(_epoch + TimeSpan.FromHours(3.1));
+ }
+ }
+}
diff --git a/src/NzbDrone.Core/Datastore/Migration/194_add_notification_status.cs b/src/NzbDrone.Core/Datastore/Migration/194_add_notification_status.cs
new file mode 100644
index 000000000..2ef9a3f42
--- /dev/null
+++ b/src/NzbDrone.Core/Datastore/Migration/194_add_notification_status.cs
@@ -0,0 +1,19 @@
+using FluentMigrator;
+using NzbDrone.Core.Datastore.Migration.Framework;
+
+namespace NzbDrone.Core.Datastore.Migration
+{
+ [Migration(194)]
+ public class add_notification_status : NzbDroneMigrationBase
+ {
+ protected override void MainDbUpgrade()
+ {
+ Create.TableForModel("NotificationStatus")
+ .WithColumn("ProviderId").AsInt32().NotNullable().Unique()
+ .WithColumn("InitialFailure").AsDateTimeOffset().Nullable()
+ .WithColumn("MostRecentFailure").AsDateTimeOffset().Nullable()
+ .WithColumn("EscalationLevel").AsInt32().NotNullable()
+ .WithColumn("DisabledTill").AsDateTimeOffset().Nullable();
+ }
+ }
+}
diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs
index be3761ae2..4ada8a42b 100644
--- a/src/NzbDrone.Core/Datastore/TableMapping.cs
+++ b/src/NzbDrone.Core/Datastore/TableMapping.cs
@@ -154,6 +154,7 @@ namespace NzbDrone.Core.Datastore
Mapper.Entity("IndexerStatus").RegisterModel();
Mapper.Entity("DownloadClientStatus").RegisterModel();
Mapper.Entity("ImportListStatus").RegisterModel();
+ Mapper.Entity("NotificationStatus").RegisterModel();
Mapper.Entity("CustomFilters").RegisterModel();
diff --git a/src/NzbDrone.Core/HealthCheck/Checks/NotificationStatusCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/NotificationStatusCheck.cs
new file mode 100644
index 000000000..c9b5e2561
--- /dev/null
+++ b/src/NzbDrone.Core/HealthCheck/Checks/NotificationStatusCheck.cs
@@ -0,0 +1,52 @@
+using System.Linq;
+using NzbDrone.Common.Extensions;
+using NzbDrone.Core.Localization;
+using NzbDrone.Core.Notifications;
+using NzbDrone.Core.ThingiProvider.Events;
+
+namespace NzbDrone.Core.HealthCheck.Checks
+{
+ [CheckOn(typeof(ProviderUpdatedEvent))]
+ [CheckOn(typeof(ProviderDeletedEvent))]
+ [CheckOn(typeof(ProviderStatusChangedEvent))]
+ public class NotificationStatusCheck : HealthCheckBase
+ {
+ private readonly INotificationFactory _providerFactory;
+ private readonly INotificationStatusService _providerStatusService;
+
+ public NotificationStatusCheck(INotificationFactory providerFactory, INotificationStatusService providerStatusService, ILocalizationService localizationService)
+ : base(localizationService)
+ {
+ _providerFactory = providerFactory;
+ _providerStatusService = providerStatusService;
+ }
+
+ public override HealthCheck Check()
+ {
+ var enabledProviders = _providerFactory.GetAvailableProviders();
+ var backOffProviders = enabledProviders.Join(_providerStatusService.GetBlockedProviders(),
+ i => i.Definition.Id,
+ s => s.ProviderId,
+ (i, s) => new { Provider = i, Status = s })
+ .ToList();
+
+ if (backOffProviders.Empty())
+ {
+ return new HealthCheck(GetType());
+ }
+
+ if (backOffProviders.Count == enabledProviders.Count)
+ {
+ return new HealthCheck(GetType(),
+ HealthCheckResult.Error,
+ _localizationService.GetLocalizedString("NotificationStatusAllClientHealthCheckMessage"),
+ "#notifications-are-unavailable-due-to-failures");
+ }
+
+ return new HealthCheck(GetType(),
+ HealthCheckResult.Warning,
+ string.Format(_localizationService.GetLocalizedString("NotificationStatusSingleClientHealthCheckMessage"), string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name))),
+ "#notifications-are-unavailable-due-to-failures");
+ }
+ }
+}
diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedNotificationStatus.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedNotificationStatus.cs
new file mode 100644
index 000000000..cfc3e1f63
--- /dev/null
+++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedNotificationStatus.cs
@@ -0,0 +1,27 @@
+using Dapper;
+using NzbDrone.Core.Datastore;
+
+namespace NzbDrone.Core.Housekeeping.Housekeepers
+{
+ public class CleanupOrphanedNotificationStatus : IHousekeepingTask
+ {
+ private readonly IMainDatabase _database;
+
+ public CleanupOrphanedNotificationStatus(IMainDatabase database)
+ {
+ _database = database;
+ }
+
+ public void Clean()
+ {
+ using var mapper = _database.OpenConnection();
+
+ mapper.Execute(@"DELETE FROM ""NotificationStatus""
+ WHERE ""Id"" IN (
+ SELECT ""NotificationStatus"".""Id"" FROM ""NotificationStatus""
+ LEFT OUTER JOIN ""Notifications""
+ ON ""NotificationStatus"".""ProviderId"" = ""Notifications"".""Id""
+ WHERE ""Notifications"".""Id"" IS NULL)");
+ }
+ }
+}
diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/FixFutureNotificationStatusTimes.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/FixFutureNotificationStatusTimes.cs
new file mode 100644
index 000000000..10af6ab42
--- /dev/null
+++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/FixFutureNotificationStatusTimes.cs
@@ -0,0 +1,12 @@
+using NzbDrone.Core.Notifications;
+
+namespace NzbDrone.Core.Housekeeping.Housekeepers
+{
+ public class FixFutureNotificationStatusTimes : FixFutureProviderStatusTimes, IHousekeepingTask
+ {
+ public FixFutureNotificationStatusTimes(INotificationStatusRepository notificationStatusRepository)
+ : base(notificationStatusRepository)
+ {
+ }
+ }
+}
diff --git a/src/NzbDrone.Core/Notifications/NotificationFactory.cs b/src/NzbDrone.Core/Notifications/NotificationFactory.cs
index 24a42ca88..56c078ed9 100644
--- a/src/NzbDrone.Core/Notifications/NotificationFactory.cs
+++ b/src/NzbDrone.Core/Notifications/NotificationFactory.cs
@@ -9,87 +9,168 @@ namespace NzbDrone.Core.Notifications
{
public interface INotificationFactory : IProviderFactory
{
- List OnGrabEnabled();
- List OnDownloadEnabled();
- List OnUpgradeEnabled();
- List OnRenameEnabled();
- List OnSeriesAddEnabled();
- List OnSeriesDeleteEnabled();
- List OnEpisodeFileDeleteEnabled();
- List OnEpisodeFileDeleteForUpgradeEnabled();
- List OnHealthIssueEnabled();
- List OnHealthRestoredEnabled();
- List OnApplicationUpdateEnabled();
- List OnManualInteractionEnabled();
+ List OnGrabEnabled(bool filterBlockedNotifications = true);
+ List OnDownloadEnabled(bool filterBlockedNotifications = true);
+ List OnUpgradeEnabled(bool filterBlockedNotifications = true);
+ List OnRenameEnabled(bool filterBlockedNotifications = true);
+ List OnSeriesAddEnabled(bool filterBlockedNotifications = true);
+ List OnSeriesDeleteEnabled(bool filterBlockedNotifications = true);
+ List OnEpisodeFileDeleteEnabled(bool filterBlockedNotifications = true);
+ List OnEpisodeFileDeleteForUpgradeEnabled(bool filterBlockedNotifications = true);
+ List OnHealthIssueEnabled(bool filterBlockedNotifications = true);
+ List OnHealthRestoredEnabled(bool filterBlockedNotifications = true);
+ List OnApplicationUpdateEnabled(bool filterBlockedNotifications = true);
+ List OnManualInteractionEnabled(bool filterBlockedNotifications = true);
}
public class NotificationFactory : ProviderFactory, INotificationFactory
{
- public NotificationFactory(INotificationRepository providerRepository, IEnumerable providers, IServiceProvider container, IEventAggregator eventAggregator, Logger logger)
+ private readonly INotificationStatusService _notificationStatusService;
+ private readonly Logger _logger;
+
+ public NotificationFactory(INotificationStatusService notificationStatusService, INotificationRepository providerRepository, IEnumerable providers, IServiceProvider container, IEventAggregator eventAggregator, Logger logger)
: base(providerRepository, providers, container, eventAggregator, logger)
{
+ _notificationStatusService = notificationStatusService;
+ _logger = logger;
}
- public List OnGrabEnabled()
+ public List OnGrabEnabled(bool filterBlockedNotifications = true)
{
+ if (filterBlockedNotifications)
+ {
+ return FilterBlockedNotifications(GetAvailableProviders().Where(n => ((NotificationDefinition)n.Definition).OnGrab)).ToList();
+ }
+
return GetAvailableProviders().Where(n => ((NotificationDefinition)n.Definition).OnGrab).ToList();
}
- public List OnDownloadEnabled()
+ public List OnDownloadEnabled(bool filterBlockedNotifications = true)
{
+ if (filterBlockedNotifications)
+ {
+ return FilterBlockedNotifications(GetAvailableProviders().Where(n => ((NotificationDefinition)n.Definition).OnDownload)).ToList();
+ }
+
return GetAvailableProviders().Where(n => ((NotificationDefinition)n.Definition).OnDownload).ToList();
}
- public List OnUpgradeEnabled()
+ public List OnUpgradeEnabled(bool filterBlockedNotifications = true)
{
+ if (filterBlockedNotifications)
+ {
+ return FilterBlockedNotifications(GetAvailableProviders().Where(n => ((NotificationDefinition)n.Definition).OnUpgrade)).ToList();
+ }
+
return GetAvailableProviders().Where(n => ((NotificationDefinition)n.Definition).OnUpgrade).ToList();
}
- public List OnRenameEnabled()
+ public List OnRenameEnabled(bool filterBlockedNotifications = true)
{
+ if (filterBlockedNotifications)
+ {
+ return FilterBlockedNotifications(GetAvailableProviders().Where(n => ((NotificationDefinition)n.Definition).OnRename)).ToList();
+ }
+
return GetAvailableProviders().Where(n => ((NotificationDefinition)n.Definition).OnRename).ToList();
}
- public List OnSeriesAddEnabled()
+ public List OnSeriesAddEnabled(bool filterBlockedNotifications = true)
{
+ if (filterBlockedNotifications)
+ {
+ return FilterBlockedNotifications(GetAvailableProviders().Where(n => ((NotificationDefinition)n.Definition).OnSeriesAdd)).ToList();
+ }
+
return GetAvailableProviders().Where(n => ((NotificationDefinition)n.Definition).OnSeriesAdd).ToList();
}
- public List OnSeriesDeleteEnabled()
+ public List OnSeriesDeleteEnabled(bool filterBlockedNotifications = true)
{
+ if (filterBlockedNotifications)
+ {
+ return FilterBlockedNotifications(GetAvailableProviders().Where(n => ((NotificationDefinition)n.Definition).OnSeriesDelete)).ToList();
+ }
+
return GetAvailableProviders().Where(n => ((NotificationDefinition)n.Definition).OnSeriesDelete).ToList();
}
- public List OnEpisodeFileDeleteEnabled()
+ public List OnEpisodeFileDeleteEnabled(bool filterBlockedNotifications = true)
{
+ if (filterBlockedNotifications)
+ {
+ return FilterBlockedNotifications(GetAvailableProviders().Where(n => ((NotificationDefinition)n.Definition).OnEpisodeFileDelete)).ToList();
+ }
+
return GetAvailableProviders().Where(n => ((NotificationDefinition)n.Definition).OnEpisodeFileDelete).ToList();
}
- public List OnEpisodeFileDeleteForUpgradeEnabled()
+ public List OnEpisodeFileDeleteForUpgradeEnabled(bool filterBlockedNotifications = true)
{
+ if (filterBlockedNotifications)
+ {
+ return FilterBlockedNotifications(GetAvailableProviders().Where(n => ((NotificationDefinition)n.Definition).OnEpisodeFileDeleteForUpgrade)).ToList();
+ }
+
return GetAvailableProviders().Where(n => ((NotificationDefinition)n.Definition).OnEpisodeFileDeleteForUpgrade).ToList();
}
- public List OnHealthIssueEnabled()
+ public List OnHealthIssueEnabled(bool filterBlockedNotifications = true)
{
+ if (filterBlockedNotifications)
+ {
+ return FilterBlockedNotifications(GetAvailableProviders().Where(n => ((NotificationDefinition)n.Definition).OnHealthIssue)).ToList();
+ }
+
return GetAvailableProviders().Where(n => ((NotificationDefinition)n.Definition).OnHealthIssue).ToList();
}
- public List OnHealthRestoredEnabled()
+ public List OnHealthRestoredEnabled(bool filterBlockedNotifications = true)
{
+ if (filterBlockedNotifications)
+ {
+ return FilterBlockedNotifications(GetAvailableProviders().Where(n => ((NotificationDefinition)n.Definition).OnHealthRestored)).ToList();
+ }
+
return GetAvailableProviders().Where(n => ((NotificationDefinition)n.Definition).OnHealthRestored).ToList();
}
- public List OnApplicationUpdateEnabled()
+ public List OnApplicationUpdateEnabled(bool filterBlockedNotifications = true)
{
+ if (filterBlockedNotifications)
+ {
+ return FilterBlockedNotifications(GetAvailableProviders().Where(n => ((NotificationDefinition)n.Definition).OnApplicationUpdate)).ToList();
+ }
+
return GetAvailableProviders().Where(n => ((NotificationDefinition)n.Definition).OnApplicationUpdate).ToList();
}
- public List OnManualInteractionEnabled()
+ public List OnManualInteractionEnabled(bool filterBlockedNotifications = true)
{
+ if (filterBlockedNotifications)
+ {
+ return FilterBlockedNotifications(GetAvailableProviders().Where(n => ((NotificationDefinition)n.Definition).OnManualInteractionRequired)).ToList();
+ }
+
return GetAvailableProviders().Where(n => ((NotificationDefinition)n.Definition).OnManualInteractionRequired).ToList();
}
+ private IEnumerable FilterBlockedNotifications(IEnumerable notifications)
+ {
+ var blockedNotifications = _notificationStatusService.GetBlockedProviders().ToDictionary(v => v.ProviderId, v => v);
+
+ foreach (var notification in notifications)
+ {
+ if (blockedNotifications.TryGetValue(notification.Definition.Id, out var notificationStatus))
+ {
+ _logger.Debug("Temporarily ignoring notification {0} till {1} due to recent failures.", notification.Definition.Name, notificationStatus.DisabledTill.Value.ToLocalTime());
+ continue;
+ }
+
+ yield return notification;
+ }
+ }
+
public override void SetProviderCharacteristics(INotification provider, NotificationDefinition definition)
{
base.SetProviderCharacteristics(provider, definition);
diff --git a/src/NzbDrone.Core/Notifications/NotificationService.cs b/src/NzbDrone.Core/Notifications/NotificationService.cs
index 46e672f1f..83966433c 100644
--- a/src/NzbDrone.Core/Notifications/NotificationService.cs
+++ b/src/NzbDrone.Core/Notifications/NotificationService.cs
@@ -32,11 +32,13 @@ namespace NzbDrone.Core.Notifications
IHandleAsync
{
private readonly INotificationFactory _notificationFactory;
+ private readonly INotificationStatusService _notificationStatusService;
private readonly Logger _logger;
- public NotificationService(INotificationFactory notificationFactory, Logger logger)
+ public NotificationService(INotificationFactory notificationFactory, INotificationStatusService notificationStatusService, Logger logger)
{
_notificationFactory = notificationFactory;
+ _notificationStatusService = notificationStatusService;
_logger = logger;
}
@@ -136,9 +138,11 @@ namespace NzbDrone.Core.Notifications
}
notification.OnGrab(grabMessage);
+ _notificationStatusService.RecordSuccess(notification.Definition.Id);
}
catch (Exception ex)
{
+ _notificationStatusService.RecordFailure(notification.Definition.Id);
_logger.Error(ex, "Unable to send OnGrab notification to {0}", notification.Definition.Name);
}
}
@@ -173,11 +177,13 @@ namespace NzbDrone.Core.Notifications
if (downloadMessage.OldFiles.Empty() || ((NotificationDefinition)notification.Definition).OnUpgrade)
{
notification.OnDownload(downloadMessage);
+ _notificationStatusService.RecordSuccess(notification.Definition.Id);
}
}
}
catch (Exception ex)
{
+ _notificationStatusService.RecordFailure(notification.Definition.Id);
_logger.Warn(ex, "Unable to send OnDownload notification to: " + notification.Definition.Name);
}
}
@@ -192,10 +198,12 @@ namespace NzbDrone.Core.Notifications
if (ShouldHandleSeries(notification.Definition, message.Series))
{
notification.OnRename(message.Series, message.RenamedFiles);
+ _notificationStatusService.RecordSuccess(notification.Definition.Id);
}
}
catch (Exception ex)
{
+ _notificationStatusService.RecordFailure(notification.Definition.Id);
_logger.Warn(ex, "Unable to send OnRename notification to: " + notification.Definition.Name);
}
}
@@ -213,9 +221,11 @@ namespace NzbDrone.Core.Notifications
try
{
notification.OnApplicationUpdate(updateMessage);
+ _notificationStatusService.RecordSuccess(notification.Definition.Id);
}
catch (Exception ex)
{
+ _notificationStatusService.RecordFailure(notification.Definition.Id);
_logger.Warn(ex, "Unable to send OnApplicationUpdate notification to: " + notification.Definition.Name);
}
}
@@ -246,9 +256,11 @@ namespace NzbDrone.Core.Notifications
}
notification.OnManualInteractionRequired(manualInteractionMessage);
+ _notificationStatusService.RecordSuccess(notification.Definition.Id);
}
catch (Exception ex)
{
+ _notificationStatusService.RecordFailure(notification.Definition.Id);
_logger.Error(ex, "Unable to send OnManualInteractionRequired notification to {0}", notification.Definition.Name);
}
}
@@ -278,11 +290,13 @@ namespace NzbDrone.Core.Notifications
if (ShouldHandleSeries(notification.Definition, deleteMessage.EpisodeFile.Series))
{
notification.OnEpisodeFileDelete(deleteMessage);
+ _notificationStatusService.RecordSuccess(notification.Definition.Id);
}
}
}
catch (Exception ex)
{
+ _notificationStatusService.RecordFailure(notification.Definition.Id);
_logger.Warn(ex, "Unable to send OnEpisodeFileDelete notification to: " + notification.Definition.Name);
}
}
@@ -304,10 +318,12 @@ namespace NzbDrone.Core.Notifications
if (ShouldHandleSeries(notification.Definition, series))
{
notification.OnSeriesAdd(addMessage);
+ _notificationStatusService.RecordSuccess(notification.Definition.Id);
}
}
catch (Exception ex)
{
+ _notificationStatusService.RecordFailure(notification.Definition.Id);
_logger.Warn(ex, "Unable to send OnSeriesAdd notification to: " + notification.Definition.Name);
}
}
@@ -326,10 +342,12 @@ namespace NzbDrone.Core.Notifications
if (ShouldHandleSeries(notification.Definition, deleteMessage.Series))
{
notification.OnSeriesDelete(deleteMessage);
+ _notificationStatusService.RecordSuccess(notification.Definition.Id);
}
}
catch (Exception ex)
{
+ _notificationStatusService.RecordFailure(notification.Definition.Id);
_logger.Warn(ex, "Unable to send OnSeriesDelete notification to: " + notification.Definition.Name);
}
}
@@ -353,10 +371,12 @@ namespace NzbDrone.Core.Notifications
if (ShouldHandleHealthFailure(message.HealthCheck, ((NotificationDefinition)notification.Definition).IncludeHealthWarnings))
{
notification.OnHealthIssue(message.HealthCheck);
+ _notificationStatusService.RecordSuccess(notification.Definition.Id);
}
}
catch (Exception ex)
{
+ _notificationStatusService.RecordFailure(notification.Definition.Id);
_logger.Warn(ex, "Unable to send OnHealthIssue notification to: " + notification.Definition.Name);
}
}
@@ -376,10 +396,12 @@ namespace NzbDrone.Core.Notifications
if (ShouldHandleHealthFailure(message.PreviousCheck, ((NotificationDefinition)notification.Definition).IncludeHealthWarnings))
{
notification.OnHealthRestored(message.PreviousCheck);
+ _notificationStatusService.RecordSuccess(notification.Definition.Id);
}
}
catch (Exception ex)
{
+ _notificationStatusService.RecordFailure(notification.Definition.Id);
_logger.Warn(ex, "Unable to send OnHealthRestored notification to: " + notification.Definition.Name);
}
}
diff --git a/src/NzbDrone.Core/Notifications/NotificationStatus.cs b/src/NzbDrone.Core/Notifications/NotificationStatus.cs
new file mode 100644
index 000000000..1cb6f4a2e
--- /dev/null
+++ b/src/NzbDrone.Core/Notifications/NotificationStatus.cs
@@ -0,0 +1,8 @@
+using NzbDrone.Core.ThingiProvider.Status;
+
+namespace NzbDrone.Core.Notifications
+{
+ public class NotificationStatus : ProviderStatusBase
+ {
+ }
+}
diff --git a/src/NzbDrone.Core/Notifications/NotificationStatusRepository.cs b/src/NzbDrone.Core/Notifications/NotificationStatusRepository.cs
new file mode 100644
index 000000000..c5e61647f
--- /dev/null
+++ b/src/NzbDrone.Core/Notifications/NotificationStatusRepository.cs
@@ -0,0 +1,18 @@
+using NzbDrone.Core.Datastore;
+using NzbDrone.Core.Messaging.Events;
+using NzbDrone.Core.ThingiProvider.Status;
+
+namespace NzbDrone.Core.Notifications
+{
+ public interface INotificationStatusRepository : IProviderStatusRepository
+ {
+ }
+
+ public class NotificationStatusRepository : ProviderStatusRepository, INotificationStatusRepository
+ {
+ public NotificationStatusRepository(IMainDatabase database, IEventAggregator eventAggregator)
+ : base(database, eventAggregator)
+ {
+ }
+ }
+}
diff --git a/src/NzbDrone.Core/Notifications/NotificationStatusService.cs b/src/NzbDrone.Core/Notifications/NotificationStatusService.cs
new file mode 100644
index 000000000..218b21ba8
--- /dev/null
+++ b/src/NzbDrone.Core/Notifications/NotificationStatusService.cs
@@ -0,0 +1,22 @@
+using System;
+using NLog;
+using NzbDrone.Common.EnvironmentInfo;
+using NzbDrone.Core.Messaging.Events;
+using NzbDrone.Core.ThingiProvider.Status;
+
+namespace NzbDrone.Core.Notifications
+{
+ public interface INotificationStatusService : IProviderStatusServiceBase
+ {
+ }
+
+ public class NotificationStatusService : ProviderStatusServiceBase, INotificationStatusService
+ {
+ public NotificationStatusService(INotificationStatusRepository providerStatusRepository, IEventAggregator eventAggregator, IRuntimeInfo runtimeInfo, Logger logger)
+ : base(providerStatusRepository, eventAggregator, runtimeInfo, logger)
+ {
+ MinimumTimeSinceInitialFailure = TimeSpan.FromMinutes(5);
+ MaximumEscalationLevel = 5;
+ }
+ }
+}