mirror of
https://github.com/Prowlarr/Prowlarr
synced 2026-05-08 12:43:19 +02:00
Merge 1f2b62b664 into 18fe4ec495
This commit is contained in:
commit
cc080f7a2f
4 changed files with 186 additions and 24 deletions
|
|
@ -0,0 +1,149 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Moq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Core.History;
|
||||||
|
using NzbDrone.Core.Indexers;
|
||||||
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.IndexerTests
|
||||||
|
{
|
||||||
|
public class IndexerLimitServiceFixture : CoreTest<IndexerLimitService>
|
||||||
|
{
|
||||||
|
private IndexerDefinition CreateIndexerWithLimitsUnit(IndexerLimitsUnit unit, int id = 1)
|
||||||
|
{
|
||||||
|
return new IndexerDefinition
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
Settings = new TestIndexerSettings
|
||||||
|
{
|
||||||
|
BaseSettings = new IndexerBaseSettings
|
||||||
|
{
|
||||||
|
LimitsUnit = (int)unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_1440_for_day_unit()
|
||||||
|
{
|
||||||
|
var indexer = CreateIndexerWithLimitsUnit(IndexerLimitsUnit.Day);
|
||||||
|
|
||||||
|
Subject.CalculateIntervalLimitMinutes(indexer).Should().Be(1440);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_60_for_hour_unit()
|
||||||
|
{
|
||||||
|
var indexer = CreateIndexerWithLimitsUnit(IndexerLimitsUnit.Hour);
|
||||||
|
|
||||||
|
Subject.CalculateIntervalLimitMinutes(indexer).Should().Be(60);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_1_for_minute_unit()
|
||||||
|
{
|
||||||
|
var indexer = CreateIndexerWithLimitsUnit(IndexerLimitsUnit.Minute);
|
||||||
|
|
||||||
|
Subject.CalculateIntervalLimitMinutes(indexer).Should().Be(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_1440_for_default_when_id_is_zero()
|
||||||
|
{
|
||||||
|
var indexer = CreateIndexerWithLimitsUnit(IndexerLimitsUnit.Hour, id: 0);
|
||||||
|
|
||||||
|
Subject.CalculateIntervalLimitMinutes(indexer).Should().Be(1440);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_format_day_interval()
|
||||||
|
{
|
||||||
|
IndexerLimitService.FormatIntervalLimit(1440).Should().Be("1 day");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_format_hour_interval()
|
||||||
|
{
|
||||||
|
IndexerLimitService.FormatIntervalLimit(60).Should().Be("1 hour");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_format_minute_interval()
|
||||||
|
{
|
||||||
|
IndexerLimitService.FormatIntervalLimit(1).Should().Be("1 minute");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_format_minutes_interval()
|
||||||
|
{
|
||||||
|
IndexerLimitService.FormatIntervalLimit(5).Should().Be("5 minute(s)");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_true_when_at_query_limit()
|
||||||
|
{
|
||||||
|
var indexer = CreateIndexerWithLimitsUnit(IndexerLimitsUnit.Minute);
|
||||||
|
((IIndexerSettings)indexer.Settings).BaseSettings.QueryLimit = 10;
|
||||||
|
|
||||||
|
Mocker.GetMock<IHistoryService>()
|
||||||
|
.Setup(s => s.CountSince(indexer.Id, It.IsAny<DateTime>(), It.Is<List<HistoryEventType>>(l => l.Contains(HistoryEventType.IndexerQuery))))
|
||||||
|
.Returns(10);
|
||||||
|
|
||||||
|
Subject.AtQueryLimit(indexer).Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_false_when_under_query_limit()
|
||||||
|
{
|
||||||
|
var indexer = CreateIndexerWithLimitsUnit(IndexerLimitsUnit.Minute);
|
||||||
|
((IIndexerSettings)indexer.Settings).BaseSettings.QueryLimit = 10;
|
||||||
|
|
||||||
|
Mocker.GetMock<IHistoryService>()
|
||||||
|
.Setup(s => s.CountSince(indexer.Id, It.IsAny<DateTime>(), It.Is<List<HistoryEventType>>(l => l.Contains(HistoryEventType.IndexerQuery))))
|
||||||
|
.Returns(9);
|
||||||
|
|
||||||
|
Subject.AtQueryLimit(indexer).Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_true_when_at_download_limit()
|
||||||
|
{
|
||||||
|
var indexer = CreateIndexerWithLimitsUnit(IndexerLimitsUnit.Hour);
|
||||||
|
((IIndexerSettings)indexer.Settings).BaseSettings.GrabLimit = 5;
|
||||||
|
|
||||||
|
Mocker.GetMock<IHistoryService>()
|
||||||
|
.Setup(s => s.CountSince(indexer.Id, It.IsAny<DateTime>(), It.Is<List<HistoryEventType>>(l => l.Contains(HistoryEventType.ReleaseGrabbed))))
|
||||||
|
.Returns(5);
|
||||||
|
|
||||||
|
Subject.AtDownloadLimit(indexer).Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_false_when_under_download_limit()
|
||||||
|
{
|
||||||
|
var indexer = CreateIndexerWithLimitsUnit(IndexerLimitsUnit.Hour);
|
||||||
|
((IIndexerSettings)indexer.Settings).BaseSettings.GrabLimit = 5;
|
||||||
|
|
||||||
|
Mocker.GetMock<IHistoryService>()
|
||||||
|
.Setup(s => s.CountSince(indexer.Id, It.IsAny<DateTime>(), It.Is<List<HistoryEventType>>(l => l.Contains(HistoryEventType.ReleaseGrabbed))))
|
||||||
|
.Returns(4);
|
||||||
|
|
||||||
|
Subject.AtDownloadLimit(indexer).Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_use_correct_time_window_for_query_limit_minutes()
|
||||||
|
{
|
||||||
|
var indexer = CreateIndexerWithLimitsUnit(IndexerLimitsUnit.Minute);
|
||||||
|
((IIndexerSettings)indexer.Settings).BaseSettings.QueryLimit = 10;
|
||||||
|
|
||||||
|
Subject.AtQueryLimit(indexer);
|
||||||
|
|
||||||
|
Mocker.GetMock<IHistoryService>()
|
||||||
|
.Verify(v => v.CountSince(indexer.Id, It.Is<DateTime>(d => d > DateTime.Now.AddMinutes(-1).AddSeconds(-5) && d < DateTime.Now.AddMinutes(-1).AddSeconds(5)), It.IsAny<List<HistoryEventType>>()), Times.Once);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -34,6 +34,7 @@ public class IndexerBaseSettings
|
||||||
public enum IndexerLimitsUnit
|
public enum IndexerLimitsUnit
|
||||||
{
|
{
|
||||||
Day = 0,
|
Day = 0,
|
||||||
Hour = 1
|
Hour = 1,
|
||||||
|
Minute = 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ public interface IIndexerLimitService
|
||||||
bool AtQueryLimit(IndexerDefinition indexer);
|
bool AtQueryLimit(IndexerDefinition indexer);
|
||||||
int CalculateRetryAfterDownloadLimit(IndexerDefinition indexer);
|
int CalculateRetryAfterDownloadLimit(IndexerDefinition indexer);
|
||||||
int CalculateRetryAfterQueryLimit(IndexerDefinition indexer);
|
int CalculateRetryAfterQueryLimit(IndexerDefinition indexer);
|
||||||
int CalculateIntervalLimitHours(IndexerDefinition indexer);
|
int CalculateIntervalLimitMinutes(IndexerDefinition indexer);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class IndexerLimitService : IIndexerLimitService
|
public class IndexerLimitService : IIndexerLimitService
|
||||||
|
|
@ -30,18 +30,18 @@ public bool AtDownloadLimit(IndexerDefinition indexer)
|
||||||
{
|
{
|
||||||
if (indexer is { Id: > 0 } && ((IIndexerSettings)indexer.Settings).BaseSettings.GrabLimit.HasValue)
|
if (indexer is { Id: > 0 } && ((IIndexerSettings)indexer.Settings).BaseSettings.GrabLimit.HasValue)
|
||||||
{
|
{
|
||||||
var intervalLimitHours = CalculateIntervalLimitHours(indexer);
|
var intervalLimitMinutes = CalculateIntervalLimitMinutes(indexer);
|
||||||
var grabCount = _historyService.CountSince(indexer.Id, DateTime.Now.AddHours(-1 * intervalLimitHours), new List<HistoryEventType> { HistoryEventType.ReleaseGrabbed });
|
var grabCount = _historyService.CountSince(indexer.Id, DateTime.Now.AddMinutes(-1 * intervalLimitMinutes), new List<HistoryEventType> { HistoryEventType.ReleaseGrabbed });
|
||||||
var grabLimit = ((IIndexerSettings)indexer.Settings).BaseSettings.GrabLimit;
|
var grabLimit = ((IIndexerSettings)indexer.Settings).BaseSettings.GrabLimit;
|
||||||
|
|
||||||
if (grabCount >= grabLimit)
|
if (grabCount >= grabLimit)
|
||||||
{
|
{
|
||||||
_logger.Info("Indexer {0} has performed {1} of possible {2} grabs in last {3} hour(s), exceeding the maximum grab limit", indexer.Name, grabCount, grabLimit, intervalLimitHours);
|
_logger.Info("Indexer {0} has performed {1} of possible {2} grabs in last {3}, exceeding the maximum grab limit", indexer.Name, grabCount, grabLimit, FormatIntervalLimit(intervalLimitMinutes));
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.Debug("Indexer {0} has performed {1} of possible {2} grabs in last {3} hour(s), proceeding", indexer.Name, grabCount, grabLimit, intervalLimitHours);
|
_logger.Debug("Indexer {0} has performed {1} of possible {2} grabs in last {3}, proceeding", indexer.Name, grabCount, grabLimit, FormatIntervalLimit(intervalLimitMinutes));
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -51,18 +51,18 @@ public bool AtQueryLimit(IndexerDefinition indexer)
|
||||||
{
|
{
|
||||||
if (indexer is { Id: > 0 } && ((IIndexerSettings)indexer.Settings).BaseSettings.QueryLimit.HasValue)
|
if (indexer is { Id: > 0 } && ((IIndexerSettings)indexer.Settings).BaseSettings.QueryLimit.HasValue)
|
||||||
{
|
{
|
||||||
var intervalLimitHours = CalculateIntervalLimitHours(indexer);
|
var intervalLimitMinutes = CalculateIntervalLimitMinutes(indexer);
|
||||||
var queryCount = _historyService.CountSince(indexer.Id, DateTime.Now.AddHours(-1 * intervalLimitHours), new List<HistoryEventType> { HistoryEventType.IndexerQuery, HistoryEventType.IndexerRss });
|
var queryCount = _historyService.CountSince(indexer.Id, DateTime.Now.AddMinutes(-1 * intervalLimitMinutes), new List<HistoryEventType> { HistoryEventType.IndexerQuery, HistoryEventType.IndexerRss });
|
||||||
var queryLimit = ((IIndexerSettings)indexer.Settings).BaseSettings.QueryLimit;
|
var queryLimit = ((IIndexerSettings)indexer.Settings).BaseSettings.QueryLimit;
|
||||||
|
|
||||||
if (queryCount >= queryLimit)
|
if (queryCount >= queryLimit)
|
||||||
{
|
{
|
||||||
_logger.Info("Indexer {0} has performed {1} of possible {2} queries in last {3} hour(s), exceeding the maximum query limit", indexer.Name, queryCount, queryLimit, intervalLimitHours);
|
_logger.Info("Indexer {0} has performed {1} of possible {2} queries in last {3}, exceeding the maximum query limit", indexer.Name, queryCount, queryLimit, FormatIntervalLimit(intervalLimitMinutes));
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.Debug("Indexer {0} has performed {1} of possible {2} queries in last {3} hour(s), proceeding", indexer.Name, queryCount, queryLimit, intervalLimitHours);
|
_logger.Debug("Indexer {0} has performed {1} of possible {2} queries in last {3}, proceeding", indexer.Name, queryCount, queryLimit, FormatIntervalLimit(intervalLimitMinutes));
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -72,14 +72,14 @@ public int CalculateRetryAfterDownloadLimit(IndexerDefinition indexer)
|
||||||
{
|
{
|
||||||
if (indexer is { Id: > 0 } && ((IIndexerSettings)indexer.Settings).BaseSettings.GrabLimit.HasValue)
|
if (indexer is { Id: > 0 } && ((IIndexerSettings)indexer.Settings).BaseSettings.GrabLimit.HasValue)
|
||||||
{
|
{
|
||||||
var intervalLimitHours = CalculateIntervalLimitHours(indexer);
|
var intervalLimitMinutes = CalculateIntervalLimitMinutes(indexer);
|
||||||
var grabLimit = ((IIndexerSettings)indexer.Settings).BaseSettings.GrabLimit.GetValueOrDefault();
|
var grabLimit = ((IIndexerSettings)indexer.Settings).BaseSettings.GrabLimit.GetValueOrDefault();
|
||||||
|
|
||||||
var firstHistorySince = _historyService.FindFirstForIndexerSince(indexer.Id, DateTime.Now.AddHours(-1 * intervalLimitHours), new List<HistoryEventType> { HistoryEventType.ReleaseGrabbed }, grabLimit);
|
var firstHistorySince = _historyService.FindFirstForIndexerSince(indexer.Id, DateTime.Now.AddMinutes(-1 * intervalLimitMinutes), new List<HistoryEventType> { HistoryEventType.ReleaseGrabbed }, grabLimit);
|
||||||
|
|
||||||
if (firstHistorySince != null)
|
if (firstHistorySince != null)
|
||||||
{
|
{
|
||||||
return Convert.ToInt32(firstHistorySince.Date.ToLocalTime().AddHours(intervalLimitHours).Subtract(DateTime.Now).TotalSeconds);
|
return Convert.ToInt32(firstHistorySince.Date.ToLocalTime().AddMinutes(intervalLimitMinutes).Subtract(DateTime.Now).TotalSeconds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -90,33 +90,45 @@ public int CalculateRetryAfterQueryLimit(IndexerDefinition indexer)
|
||||||
{
|
{
|
||||||
if (indexer is { Id: > 0 } && ((IIndexerSettings)indexer.Settings).BaseSettings.QueryLimit.HasValue)
|
if (indexer is { Id: > 0 } && ((IIndexerSettings)indexer.Settings).BaseSettings.QueryLimit.HasValue)
|
||||||
{
|
{
|
||||||
var intervalLimitHours = CalculateIntervalLimitHours(indexer);
|
var intervalLimitMinutes = CalculateIntervalLimitMinutes(indexer);
|
||||||
var queryLimit = ((IIndexerSettings)indexer.Settings).BaseSettings.QueryLimit.GetValueOrDefault();
|
var queryLimit = ((IIndexerSettings)indexer.Settings).BaseSettings.QueryLimit.GetValueOrDefault();
|
||||||
|
|
||||||
var firstHistorySince = _historyService.FindFirstForIndexerSince(indexer.Id, DateTime.Now.AddHours(-1 * intervalLimitHours), new List<HistoryEventType> { HistoryEventType.IndexerQuery, HistoryEventType.IndexerRss }, queryLimit);
|
var firstHistorySince = _historyService.FindFirstForIndexerSince(indexer.Id, DateTime.Now.AddMinutes(-1 * intervalLimitMinutes), new List<HistoryEventType> { HistoryEventType.IndexerQuery, HistoryEventType.IndexerRss }, queryLimit);
|
||||||
|
|
||||||
if (firstHistorySince != null)
|
if (firstHistorySince != null)
|
||||||
{
|
{
|
||||||
return Convert.ToInt32(firstHistorySince.Date.ToLocalTime().AddHours(intervalLimitHours).Subtract(DateTime.Now).TotalSeconds);
|
return Convert.ToInt32(firstHistorySince.Date.ToLocalTime().AddMinutes(intervalLimitMinutes).Subtract(DateTime.Now).TotalSeconds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int CalculateIntervalLimitHours(IndexerDefinition indexer)
|
public int CalculateIntervalLimitMinutes(IndexerDefinition indexer)
|
||||||
{
|
{
|
||||||
if (indexer is { Id: > 0 })
|
if (indexer is { Id: > 0 })
|
||||||
{
|
{
|
||||||
return ((IIndexerSettings)indexer.Settings).BaseSettings.LimitsUnit switch
|
return ((IIndexerSettings)indexer.Settings).BaseSettings.LimitsUnit switch
|
||||||
{
|
{
|
||||||
(int)IndexerLimitsUnit.Hour => 1,
|
(int)IndexerLimitsUnit.Minute => 1,
|
||||||
_ => 24
|
(int)IndexerLimitsUnit.Hour => 60,
|
||||||
|
_ => 1440
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to limits per day
|
// Fallback to limits per day
|
||||||
return 24;
|
return 1440;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string FormatIntervalLimit(int minutes)
|
||||||
|
{
|
||||||
|
return minutes switch
|
||||||
|
{
|
||||||
|
1440 => "1 day",
|
||||||
|
60 => "1 hour",
|
||||||
|
1 => "1 minute",
|
||||||
|
_ => $"{minutes} minute(s)"
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -162,9 +162,9 @@ public async Task<IActionResult> GetNewznabResponse(int id, [FromQuery] NewznabR
|
||||||
AddRetryAfterHeader(retryAfterQueryLimit);
|
AddRetryAfterHeader(retryAfterQueryLimit);
|
||||||
|
|
||||||
var queryLimit = ((IIndexerSettings)indexer.Definition.Settings).BaseSettings.QueryLimit;
|
var queryLimit = ((IIndexerSettings)indexer.Definition.Settings).BaseSettings.QueryLimit;
|
||||||
var intervalLimitHours = _indexerLimitService.CalculateIntervalLimitHours(indexerDef);
|
var intervalLimitMinutes = _indexerLimitService.CalculateIntervalLimitMinutes(indexerDef);
|
||||||
|
|
||||||
return CreateResponse(CreateErrorXML(429, $"User configurable Indexer Query Limit of {queryLimit} in last {intervalLimitHours} hour(s) reached."), statusCode: StatusCodes.Status429TooManyRequests);
|
return CreateResponse(CreateErrorXML(429, $"User configurable Indexer Query Limit of {queryLimit} in last {IndexerLimitService.FormatIntervalLimit(intervalLimitMinutes)} reached."), statusCode: StatusCodes.Status429TooManyRequests);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (requestType)
|
switch (requestType)
|
||||||
|
|
@ -240,9 +240,9 @@ public async Task<object> GetDownload(int id, string link, string file)
|
||||||
AddRetryAfterHeader(retryAfterDownloadLimit);
|
AddRetryAfterHeader(retryAfterDownloadLimit);
|
||||||
|
|
||||||
var grabLimit = ((IIndexerSettings)indexer.Definition.Settings).BaseSettings.GrabLimit;
|
var grabLimit = ((IIndexerSettings)indexer.Definition.Settings).BaseSettings.GrabLimit;
|
||||||
var intervalLimitHours = _indexerLimitService.CalculateIntervalLimitHours(indexerDef);
|
var intervalLimitMinutes = _indexerLimitService.CalculateIntervalLimitMinutes(indexerDef);
|
||||||
|
|
||||||
return CreateResponse(CreateErrorXML(429, $"User configurable Indexer Grab Limit of {grabLimit} in last {intervalLimitHours} hour(s) reached."), statusCode: StatusCodes.Status429TooManyRequests);
|
return CreateResponse(CreateErrorXML(429, $"User configurable Indexer Grab Limit of {grabLimit} in last {IndexerLimitService.FormatIntervalLimit(intervalLimitMinutes)} reached."), statusCode: StatusCodes.Status429TooManyRequests);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (link.IsNullOrWhiteSpace() || file.IsNullOrWhiteSpace())
|
if (link.IsNullOrWhiteSpace() || file.IsNullOrWhiteSpace())
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue