diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/UsageTrigger.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/UsageTrigger.cs index 6e4ac5104..60f266d78 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/UsageTrigger.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/UsageTrigger.cs @@ -16,6 +16,8 @@ namespace Squidex.Domain.Apps.Core.Rules.Triggers public int Limit { get; set; } + public int? NumDays { get; set; } + public override T Accept(IRuleTriggerVisitor visitor) { return visitor.Visit(this); diff --git a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/IUsageTrackerGrain.cs b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/IUsageTrackerGrain.cs index 87582bcce..9027be9b4 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/IUsageTrackerGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/IUsageTrackerGrain.cs @@ -15,10 +15,10 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking { public interface IUsageTrackerGrain : IGrainWithStringKey, IBackgroundGrain { - Task AddTargetAsync(Guid ruleId, NamedId appId, int limits); + Task AddTargetAsync(Guid ruleId, NamedId appId, int limits, int? numDays); Task RemoveTargetAsync(Guid ruleId); - Task UpdateTargetAsync(Guid ruleId, int limits); + Task UpdateTargetAsync(Guid ruleId, int limits, int? numDays); } } \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs index a287add24..9cd7202b7 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs @@ -36,21 +36,23 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking break; case CreateRule createRule: { - if (createRule.Trigger is UsageTrigger createdTrigger) + if (createRule.Trigger is UsageTrigger usage) { - await usageTrackerGrain.AddTargetAsync(createRule.RuleId, createRule.AppId, createdTrigger.Limit); + await usageTrackerGrain.AddTargetAsync(createRule.RuleId, createRule.AppId, usage.Limit, usage.NumDays); } break; } case UpdateRule ruleUpdated: - if (ruleUpdated.Trigger is UsageTrigger updatedTrigger) { - await usageTrackerGrain.UpdateTargetAsync(ruleUpdated.RuleId, updatedTrigger.Limit); - } + if (ruleUpdated.Trigger is UsageTrigger usage) + { + await usageTrackerGrain.UpdateTargetAsync(ruleUpdated.RuleId, usage.Limit, usage.NumDays); + } - break; + break; + } } await next(); diff --git a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs index 38b4cdcf8..3eec572b0 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs @@ -24,12 +24,15 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking [Reentrant] public sealed class UsageTrackerGrain : GrainOfString, IRemindable, IUsageTrackerGrain { + private const int MaxDays = 30; private readonly IUsageTracker usageTracker; public sealed class Target { public int Limits { get; set; } + public int? NumDays { get; set; } + public DateTime? Triggered { get; set; } public NamedId AppId { get; set; } @@ -75,11 +78,13 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking foreach (var kvp in State.Targets) { - var appId = kvp.Value.AppId; + var target = kvp.Value; + + var (from, to) = GetDateRange(today, target.NumDays); - if (!kvp.Value.Triggered.HasValue || !IsSameMonth(today, kvp.Value.Triggered.Value)) + if (!target.Triggered.HasValue || target.Triggered < from) { - var usage = await usageTracker.GetMonthlyCallsAsync(appId.Id.ToString(), today); + var usage = await usageTracker.GetMonthlyCallsAsync(target.AppId.Id.ToString(), today); var limit = kvp.Value.Limits; @@ -89,7 +94,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking var @event = new AppUsageExceeded { - AppId = appId, + AppId = target.AppId, CallsCurrent = usage, CallsLimit = limit, RuleId = kvp.Key @@ -103,21 +108,28 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking await WriteStateAsync(); } - private static bool IsSameMonth(DateTime lhs, DateTime rhs) + private (DateTime, DateTime) GetDateRange(DateTime today, int? numDays) { - return lhs.Year == rhs.Year && lhs.Month == rhs.Month; + if (numDays > 0 && numDays < MaxDays) + { + return (today.AddDays(-numDays.Value).AddDays(1), today); + } + else + { + return (new DateTime(today.Year, today.Month, 1), today); + } } - public Task AddTargetAsync(Guid ruleId, NamedId appId, int limits) + public Task AddTargetAsync(Guid ruleId, NamedId appId, int limits, int? numDays) { - UpdateTarget(ruleId, t => { t.Limits = limits; t.AppId = appId; }); + UpdateTarget(ruleId, t => { t.Limits = limits; t.AppId = appId; t.NumDays = numDays; }); return WriteStateAsync(); } - public Task UpdateTargetAsync(Guid ruleId, int limits) + public Task UpdateTargetAsync(Guid ruleId, int limits, int? numDays) { - UpdateTarget(ruleId, t => t.Limits = limits); + UpdateTarget(ruleId, t => { t.Limits = limits; t.NumDays = numDays; }); return WriteStateAsync(); } diff --git a/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs b/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs index 2cb0240f7..e82fabe02 100644 --- a/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs +++ b/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs @@ -167,16 +167,18 @@ namespace Squidex.Infrastructure.UsageTracking return result; } - public async Task GetMonthlyCallsAsync(string key, DateTime date) + public Task GetMonthlyCallsAsync(string key, DateTime date) + { + return GetPreviousCallsAsync(key, new DateTime(date.Year, date.Month, 1), date); + } + + public async Task GetPreviousCallsAsync(string key, DateTime fromDate, DateTime toDate) { key = GetKey(key); ThrowIfDisposed(); - var dateFrom = new DateTime(date.Year, date.Month, 1); - var dateTo = dateFrom.AddMonths(1).AddDays(-1); - - var originalUsages = await usageRepository.QueryAsync(key, dateFrom, dateTo); + var originalUsages = await usageRepository.QueryAsync(key, fromDate, toDate); return originalUsages.Sum(x => (long)x.Counters.Get(CounterTotalCalls)); } diff --git a/src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs b/src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs index 7998c422a..ddd32e051 100644 --- a/src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs +++ b/src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs @@ -14,7 +14,7 @@ namespace Squidex.Infrastructure.UsageTracking { public sealed class CachingUsageTracker : CachingProviderBase, IUsageTracker { - private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10); + private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5); private readonly IUsageTracker inner; public CachingUsageTracker(IUsageTracker inner, IMemoryCache cache) @@ -43,7 +43,7 @@ namespace Squidex.Infrastructure.UsageTracking { Guard.NotNull(key, nameof(key)); - var cacheKey = string.Concat(key, date); + var cacheKey = string.Join("$", "Usage", nameof(GetMonthlyCallsAsync), key, date); return Cache.GetOrCreateAsync(cacheKey, entry => { @@ -52,5 +52,19 @@ namespace Squidex.Infrastructure.UsageTracking return inner.GetMonthlyCallsAsync(key, date); }); } + + public Task GetPreviousCallsAsync(string key, DateTime fromDate, DateTime toDate) + { + Guard.NotNull(key, nameof(key)); + + var cacheKey = string.Join("$", "Usage", nameof(GetPreviousCallsAsync), key, fromDate, toDate); + + return Cache.GetOrCreateAsync(cacheKey, entry => + { + entry.AbsoluteExpirationRelativeToNow = CacheDuration; + + return inner.GetPreviousCallsAsync(key, fromDate, toDate); + }); + } } } diff --git a/src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs b/src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs index f0945d1e1..bb94d83ff 100644 --- a/src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs +++ b/src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs @@ -17,6 +17,8 @@ namespace Squidex.Infrastructure.UsageTracking Task GetMonthlyCallsAsync(string key, DateTime date); + Task GetPreviousCallsAsync(string key, DateTime fromDate, DateTime toDate); + Task>> QueryAsync(string key, DateTime fromDate, DateTime toDate); } } diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageRuleTriggerDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageRuleTriggerDto.cs index 00cc24b41..97e24e087 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageRuleTriggerDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageRuleTriggerDto.cs @@ -18,6 +18,11 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models.Triggers /// public int Limit { get; set; } + /// + /// The number of days to check or null for the current month. + /// + public int? NumDays { get; set; } + public override RuleTrigger ToTrigger() { return SimpleMapper.Map(this, new UsageTrigger()); diff --git a/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs b/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs index f8fd85490..cf9692bcb 100644 --- a/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs +++ b/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs @@ -64,7 +64,7 @@ namespace Squidex.Infrastructure.UsageTracking new StoredUsage("category1", date.AddDays(7), Counters(17, 22)) }; - A.CallTo(() => usageStore.QueryAsync($"{key}_API", new DateTime(2016, 1, 1), new DateTime(2016, 1, 31))) + A.CallTo(() => usageStore.QueryAsync($"{key}_API", new DateTime(2016, 1, 1), new DateTime(2016, 1, 15))) .Returns(originalData); var result = await sut.GetMonthlyCallsAsync(key, date); @@ -72,6 +72,28 @@ namespace Squidex.Infrastructure.UsageTracking Assert.Equal(55, result); } + [Fact] + public async Task Should_sum_up_when_getting_last_calls_calls() + { + var f = DateTime.Today; + var t = DateTime.Today.AddDays(10); + + IReadOnlyList originalData = new List + { + new StoredUsage("category1", f.AddDays(1), Counters(10, 15)), + new StoredUsage("category1", f.AddDays(3), Counters(13, 18)), + new StoredUsage("category1", f.AddDays(5), Counters(15, 20)), + new StoredUsage("category1", f.AddDays(7), Counters(17, 22)) + }; + + A.CallTo(() => usageStore.QueryAsync($"{key}_API", f, t)) + .Returns(originalData); + + var result = await sut.GetPreviousCallsAsync(key, f, t); + + Assert.Equal(55, result); + } + [Fact] public async Task Should_fill_missing_days() { diff --git a/tests/Squidex.Infrastructure.Tests/UsageTracking/CachingUsageTrackerTests.cs b/tests/Squidex.Infrastructure.Tests/UsageTracking/CachingUsageTrackerTests.cs index 4ceb57a93..a8fa75b46 100644 --- a/tests/Squidex.Infrastructure.Tests/UsageTracking/CachingUsageTrackerTests.cs +++ b/tests/Squidex.Infrastructure.Tests/UsageTracking/CachingUsageTrackerTests.cs @@ -59,5 +59,24 @@ namespace Squidex.Infrastructure.UsageTracking A.CallTo(() => inner.GetMonthlyCallsAsync(key, DateTime.Today)) .MustHaveHappened(Repeated.Exactly.Once); } + + [Fact] + public async Task Should_cache_days_usage() + { + var f = DateTime.Today; + var t = DateTime.Today.AddDays(10); + + A.CallTo(() => inner.GetPreviousCallsAsync(key, f, t)) + .Returns(120); + + var result1 = await sut.GetPreviousCallsAsync(key, f, t); + var result2 = await sut.GetPreviousCallsAsync(key, f, t); + + Assert.Equal(120, result1); + Assert.Equal(120, result2); + + A.CallTo(() => inner.GetPreviousCallsAsync(key, f, t)) + .MustHaveHappened(Repeated.Exactly.Once); + } } }