Browse Source

Fixes for usage notification.

pull/636/head
Sebastian 5 years ago
parent
commit
bb731bf2bc
  1. 22
      backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/UsageGate.cs
  2. 17
      backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/UsageNotifierGrain.cs
  3. 26
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Plans/UsageGateTests.cs
  4. 179
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Plans/UsageNotifierGrainTests.cs

22
backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/UsageGate.cs

@ -54,21 +54,21 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans
var (plan, _) = appPlansProvider.GetPlanForApp(app); var (plan, _) = appPlansProvider.GetPlanForApp(app);
if (plan.MaxApiCalls > 0 || plan.BlockingApiCalls > 0) var limit = plan.MaxApiCalls;
if (limit > 0 || plan.BlockingApiCalls > 0)
{ {
var usage = await apiUsageTracker.GetMonthCallsAsync(appId.ToString(), today, null); var usage = await apiUsageTracker.GetMonthCallsAsync(appId.ToString(), today, null);
if (IsAboutToBeLocked(today, plan.MaxApiCalls, usage) && !HasNotifiedBefore(app.Id)) if (IsOver10Percent(limit, usage) && IsAboutToBeLocked(today, limit, usage) && !HasNotifiedBefore(app.Id))
{ {
var users = app.Contributors.Where(x => x.Value == Role.Owner).Select(x => x.Key).ToArray();
var notification = new UsageNotification var notification = new UsageNotification
{ {
AppId = appId, AppId = appId,
AppName = app.Name, AppName = app.Name,
Usage = usage, Usage = usage,
UsageLimit = plan.MaxApiCalls, UsageLimit = limit,
Users = users Users = GetUsers(app)
}; };
GetGrain().NotifyAsync(notification).Forget(); GetGrain().NotifyAsync(notification).Forget();
@ -97,6 +97,16 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans
return memoryCache.Set(appId, true, TimeSpan.FromHours(1)); return memoryCache.Set(appId, true, TimeSpan.FromHours(1));
} }
private static string[] GetUsers(IAppEntity app)
{
return app.Contributors.Where(x => x.Value == Role.Owner).Select(x => x.Key).ToArray();
}
private static bool IsOver10Percent(long limit, long usage)
{
return usage > limit * 0.1;
}
private static bool IsAboutToBeLocked(DateTime today, long limit, long usage) private static bool IsAboutToBeLocked(DateTime today, long limit, long usage)
{ {
var daysInMonth = DateTime.DaysInMonth(today.Year, today.Month); var daysInMonth = DateTime.DaysInMonth(today.Year, today.Month);

17
backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/UsageNotifierGrain.cs

@ -8,6 +8,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using NodaTime;
using Squidex.Domain.Apps.Entities.Notifications; using Squidex.Domain.Apps.Entities.Notifications;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Orleans;
@ -19,10 +20,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans
{ {
public sealed class UsageNotifierGrain : GrainOfString, IUsageNotifierGrain public sealed class UsageNotifierGrain : GrainOfString, IUsageNotifierGrain
{ {
private static readonly TimeSpan TimeBetweenNotifications = TimeSpan.FromHours(12); private static readonly TimeSpan TimeBetweenNotifications = TimeSpan.FromDays(3);
private readonly IGrainState<State> state; private readonly IGrainState<State> state;
private readonly INotificationSender notificationSender; private readonly INotificationSender notificationSender;
private readonly IUserResolver userResolver; private readonly IUserResolver userResolver;
private readonly IClock clock;
[CollectionName("UsageNotifications")] [CollectionName("UsageNotifications")]
public sealed class State public sealed class State
@ -30,20 +32,27 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans
public Dictionary<DomainId, DateTime> NotificationsSent { get; } = new Dictionary<DomainId, DateTime>(); public Dictionary<DomainId, DateTime> NotificationsSent { get; } = new Dictionary<DomainId, DateTime>();
} }
public UsageNotifierGrain(IGrainState<State> state, INotificationSender notificationSender, IUserResolver userResolver) public UsageNotifierGrain(IGrainState<State> state, INotificationSender notificationSender, IUserResolver userResolver, IClock clock)
{ {
Guard.NotNull(state, nameof(state)); Guard.NotNull(state, nameof(state));
Guard.NotNull(notificationSender, nameof(notificationSender)); Guard.NotNull(notificationSender, nameof(notificationSender));
Guard.NotNull(userResolver, nameof(userResolver)); Guard.NotNull(userResolver, nameof(userResolver));
Guard.NotNull(clock, nameof(clock));
this.state = state; this.state = state;
this.notificationSender = notificationSender; this.notificationSender = notificationSender;
this.userResolver = userResolver; this.userResolver = userResolver;
this.clock = clock;
} }
public async Task NotifyAsync(UsageNotification notification) public async Task NotifyAsync(UsageNotification notification)
{ {
var now = DateTime.UtcNow; if (!notificationSender.IsActive)
{
return;
}
var now = clock.GetCurrentInstant().ToDateTimeUtc();
if (!HasBeenSentBefore(notification.AppId, now)) if (!HasBeenSentBefore(notification.AppId, now))
{ {
@ -73,7 +82,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans
{ {
var elapsed = now - lastSent; var elapsed = now - lastSent;
return elapsed > TimeBetweenNotifications; return elapsed < TimeBetweenNotifications;
} }
return false; return false;

26
backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Plans/UsageGateTests.cs

@ -26,10 +26,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans
private readonly IApiUsageTracker usageTracker = A.Fake<IApiUsageTracker>(); private readonly IApiUsageTracker usageTracker = A.Fake<IApiUsageTracker>();
private readonly IGrainFactory grainFactory = A.Fake<IGrainFactory>(); private readonly IGrainFactory grainFactory = A.Fake<IGrainFactory>();
private readonly IUsageNotifierGrain usageNotifierGrain = A.Fake<IUsageNotifierGrain>(); private readonly IUsageNotifierGrain usageNotifierGrain = A.Fake<IUsageNotifierGrain>();
private readonly DateTime today = new DateTime(2020, 04, 10);
private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app");
private readonly string clientId = Guid.NewGuid().ToString(); private readonly string clientId = Guid.NewGuid().ToString();
private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app");
private readonly UsageGate sut; private readonly UsageGate sut;
private DateTime today = new DateTime(2020, 10, 3);
private long apiCallsBlocking; private long apiCallsBlocking;
private long apiCallsMax; private long apiCallsMax;
private long apiCallsCurrent; private long apiCallsCurrent;
@ -110,7 +110,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans
[Fact] [Fact]
public async Task Should_return_false_and_notify_if_about_to_over_included_contingent() public async Task Should_return_false_and_notify_if_about_to_over_included_contingent()
{ {
apiCallsCurrent = 1200; // * 30 days = 3600 apiCallsCurrent = 1200; // in 10 days = 4000 / month
apiCallsBlocking = 5000; apiCallsBlocking = 5000;
apiCallsMax = 3000; apiCallsMax = 3000;
@ -125,7 +125,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans
[Fact] [Fact]
public async Task Should_return_false_and_notify_if_about_to_over_included_contingent_but_no_max_given() public async Task Should_return_false_and_notify_if_about_to_over_included_contingent_but_no_max_given()
{ {
apiCallsCurrent = 1200; // * 30 days = 3600 apiCallsCurrent = 1200; // in 10 days = 4000 / month
apiCallsBlocking = 5000; apiCallsBlocking = 5000;
apiCallsMax = 0; apiCallsMax = 0;
@ -140,7 +140,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans
[Fact] [Fact]
public async Task Should_only_notify_once_if_about_to_be_over_included_contingent() public async Task Should_only_notify_once_if_about_to_be_over_included_contingent()
{ {
apiCallsCurrent = 1200; // * 30 days = 3600 apiCallsCurrent = 1200; // in 10 days = 4000 / month
apiCallsBlocking = 5000; apiCallsBlocking = 5000;
apiCallsMax = 3000; apiCallsMax = 3000;
@ -150,5 +150,21 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans
A.CallTo(() => usageNotifierGrain.NotifyAsync(A<UsageNotification>.Ignored)) A.CallTo(() => usageNotifierGrain.NotifyAsync(A<UsageNotification>.Ignored))
.MustHaveHappenedOnceExactly(); .MustHaveHappenedOnceExactly();
} }
[Fact]
public async Task Should_not_notify_if_lower_than_10_percent()
{
today = new DateTime(2020, 10, 2);
apiCallsCurrent = 220; // in 3 days = 3300 / month
apiCallsBlocking = 5000;
apiCallsMax = 3000;
await sut.IsBlockedAsync(appEntity, clientId, today);
await sut.IsBlockedAsync(appEntity, clientId, today);
A.CallTo(() => usageNotifierGrain.NotifyAsync(A<UsageNotification>.Ignored))
.MustNotHaveHappened();
}
} }
} }

179
backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Plans/UsageNotifierGrainTests.cs

@ -0,0 +1,179 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using FakeItEasy;
using NodaTime;
using Squidex.Domain.Apps.Entities.Notifications;
using Squidex.Infrastructure.Orleans;
using Squidex.Shared.Users;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Apps.Plans
{
public class UsageNotifierGrainTests
{
private readonly IGrainState<UsageNotifierGrain.State> state = A.Fake<IGrainState<UsageNotifierGrain.State>>();
private readonly INotificationSender notificationSender = A.Fake<INotificationSender>();
private readonly IUserResolver userResolver = A.Fake<IUserResolver>();
private readonly IClock clock = A.Fake<IClock>();
private readonly UsageNotifierGrain sut;
private Instant time = SystemClock.Instance.GetCurrentInstant();
public UsageNotifierGrainTests()
{
A.CallTo(() => clock.GetCurrentInstant())
.ReturnsLazily(() => time);
A.CallTo(() => notificationSender.IsActive)
.Returns(true);
sut = new UsageNotifierGrain(state, notificationSender, userResolver, clock);
}
[Fact]
public async Task Should_not_send_notification_if_not_active()
{
SetupUser("1", null);
SetupUser("2", null);
var notification = new UsageNotification
{
AppName = "my-app",
Usage = 1000,
UsageLimit = 3000,
Users = new[] { "1", "2" }
};
await sut.NotifyAsync(notification);
A.CallTo(() => notificationSender.SendUsageAsync(A<IUser>._, "my-app", 1000, 3000))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_notify_found_users()
{
var user1 = SetupUser("1", "user1@email.com");
var user2 = SetupUser("2", "user2@email.com");
var user3 = SetupUser("3", null);
var notification = new UsageNotification
{
AppName = "my-app",
Usage = 1000,
UsageLimit = 3000,
Users = new[] { "1", "2", "3" }
};
await sut.NotifyAsync(notification);
A.CallTo(() => notificationSender.SendUsageAsync(user1!, "my-app", 1000, 3000))
.MustHaveHappened();
A.CallTo(() => notificationSender.SendUsageAsync(user2!, "my-app", 1000, 3000))
.MustHaveHappened();
A.CallTo(() => notificationSender.SendUsageAsync(user3!, "my-app", 1000, 3000))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_not_notify_again()
{
var user = SetupUser("1", "user1@email.com");
var notification = new UsageNotification
{
AppName = "my-app",
Usage = 1000,
UsageLimit = 3000,
Users = new[] { "1" }
};
await sut.NotifyAsync(notification);
await sut.NotifyAsync(notification);
A.CallTo(() => notificationSender.SendUsageAsync(user!, "my-app", 1000, 3000))
.MustHaveHappenedOnceExactly();
}
[Fact]
public async Task Should_send_again_after_3_days()
{
var user = SetupUser("1", "user1@email.com");
var notification = new UsageNotification
{
AppName = "my-app",
Usage = 1000,
UsageLimit = 3000,
Users = new[] { "1" }
};
await sut.NotifyAsync(notification);
time = time.Plus(Duration.FromDays(3));
await sut.NotifyAsync(notification);
A.CallTo(() => notificationSender.SendUsageAsync(user!, "my-app", 1000, 3000))
.MustHaveHappenedTwiceExactly();
}
[Theory]
[InlineData(1)]
[InlineData(6)]
[InlineData(12)]
[InlineData(24)]
[InlineData(48)]
public async Task Should_not_notify_again_after_few_hours(int hours)
{
var user = SetupUser("1", "user1@email.com");
var notification = new UsageNotification
{
AppName = "my-app",
Usage = 1000,
UsageLimit = 3000,
Users = new[] { "1" }
};
await sut.NotifyAsync(notification);
time = time.Plus(Duration.FromHours(hours));
await sut.NotifyAsync(notification);
A.CallTo(() => notificationSender.SendUsageAsync(user!, "my-app", 1000, 3000))
.MustHaveHappenedOnceExactly();
}
private IUser? SetupUser(string id, string? email)
{
if (email != null)
{
var user = A.Fake<IUser>();
A.CallTo(() => user.Email)
.Returns(email);
A.CallTo(() => userResolver.FindByIdOrEmailAsync(id))
.Returns(user);
return user;
}
else
{
A.CallTo(() => userResolver.FindByIdOrEmailAsync(id))
.Returns(Task.FromResult<IUser?>(null));
return null;
}
}
}
}
Loading…
Cancel
Save