Browse Source

Bugfixes in usage tracker.

pull/335/head
Sebastian Stehle 8 years ago
parent
commit
46e27a82a2
  1. 4
      src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker.cs
  2. 7
      src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker_EventHandling.cs
  3. 2
      src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs
  4. 27
      src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs
  5. 16
      src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs
  6. 6
      src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs
  7. 4
      src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs
  8. 4
      src/Squidex/Pipeline/ApiCostsFilter.cs
  9. 49
      tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs
  10. 21
      tests/Squidex.Infrastructure.Tests/UsageTracking/CachingUsageTrackerTests.cs
  11. 12
      tests/Squidex.Tests/Pipeline/ApiCostsFilterTests.cs

4
src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker.cs

@ -34,7 +34,9 @@ namespace Squidex.Domain.Apps.Entities.Assets
public async Task<long> GetTotalSizeAsync(Guid appId)
{
var entries = await usageStore.QueryAsync(appId.ToString(), SummaryDate, SummaryDate);
var key = GetKey(appId);
var entries = await usageStore.QueryAsync(key, SummaryDate, SummaryDate);
return (long)entries.Select(x => x.Counters.Get(CounterTotalSize)).FirstOrDefault();
}

7
src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker_EventHandling.cs

@ -60,11 +60,16 @@ namespace Squidex.Domain.Apps.Entities.Assets
[CounterTotalCount] = count
};
var key = appId.ToString();
var key = GetKey(appId);
return Task.WhenAll(
usageStore.TrackUsagesAsync(new UsageUpdate(date, key, Category, counters)),
usageStore.TrackUsagesAsync(new UsageUpdate(SummaryDate, key, Category, counters)));
}
private static string GetKey(Guid appId)
{
return $"{appId}_Assets";
}
}
}

2
src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs

@ -251,7 +251,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
if ((properties.Editor == TagsFieldEditor.Checkboxes || properties.Editor == TagsFieldEditor.Dropdown) && (properties.AllowedValues == null || properties.AllowedValues.Count == 0))
{
yield return new ValidationError("Chekboxes or dropdown list need allowed values.",
yield return new ValidationError("Checkboxes or dropdown list need allowed values.",
nameof(properties.AllowedValues));
}

27
src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs

@ -96,15 +96,15 @@ namespace Squidex.Infrastructure.UsageTracking
}
}
public Task TrackAsync(string key, string category, double weight, double elapsedMs)
public Task TrackAsync(Guid appId, string category, double weight, double elapsedMs)
{
Guard.NotNull(key, nameof(key));
var key = GetKey(appId);
ThrowIfDisposed();
if (weight > 0)
{
category = CleanCategory(category);
category = GetCategory(category);
usages.AddOrUpdate((key, category), _ => new Usage(elapsedMs, weight), (k, x) => x.Add(elapsedMs, weight));
}
@ -112,14 +112,14 @@ namespace Squidex.Infrastructure.UsageTracking
return TaskHelper.Done;
}
public async Task<IReadOnlyDictionary<string, IReadOnlyList<DateUsage>>> QueryAsync(string key, DateTime fromDate, DateTime toDate)
public async Task<IReadOnlyDictionary<string, IReadOnlyList<DateUsage>>> QueryAsync(Guid appId, DateTime fromDate, DateTime toDate)
{
Guard.NotNull(key, nameof(key));
ThrowIfDisposed();
var key = GetKey(appId);
var usagesFlat = await usageRepository.QueryAsync(key, fromDate, toDate);
var usagesByCategory = usagesFlat.GroupBy(x => CleanCategory(x.Category)).ToDictionary(x => x.Key, x => x.ToList());
var usagesByCategory = usagesFlat.GroupBy(x => GetCategory(x.Category)).ToDictionary(x => x.Key, x => x.ToList());
var result = new Dictionary<string, IReadOnlyList<DateUsage>>();
@ -167,12 +167,12 @@ namespace Squidex.Infrastructure.UsageTracking
return result;
}
public async Task<long> GetMonthlyCallsAsync(string key, DateTime date)
public async Task<long> GetMonthlyCallsAsync(Guid appId, DateTime date)
{
Guard.NotNull(key, nameof(key));
ThrowIfDisposed();
var key = GetKey(appId);
var dateFrom = new DateTime(date.Year, date.Month, 1);
var dateTo = dateFrom.AddMonths(1).AddDays(-1);
@ -181,9 +181,14 @@ namespace Squidex.Infrastructure.UsageTracking
return originalUsages.Sum(x => (long)x.Counters.Get(CounterTotalCalls));
}
private static string CleanCategory(string category)
private static string GetCategory(string category)
{
return !string.IsNullOrWhiteSpace(category) ? category.Trim() : "*";
}
private static string GetKey(Guid appId)
{
return $"{appId}_API";
}
}
}

16
src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs

@ -25,27 +25,25 @@ namespace Squidex.Infrastructure.UsageTracking
this.inner = inner;
}
public Task<IReadOnlyDictionary<string, IReadOnlyList<DateUsage>>> QueryAsync(string key, DateTime fromDate, DateTime toDate)
public Task<IReadOnlyDictionary<string, IReadOnlyList<DateUsage>>> QueryAsync(Guid appId, DateTime fromDate, DateTime toDate)
{
return inner.QueryAsync(key, fromDate, toDate);
return inner.QueryAsync(appId, fromDate, toDate);
}
public Task TrackAsync(string key, string category, double weight, double elapsedMs)
public Task TrackAsync(Guid appId, string category, double weight, double elapsedMs)
{
return inner.TrackAsync(key, category, weight, elapsedMs);
return inner.TrackAsync(appId, category, weight, elapsedMs);
}
public Task<long> GetMonthlyCallsAsync(string key, DateTime date)
public Task<long> GetMonthlyCallsAsync(Guid appId, DateTime date)
{
Guard.NotNull(key, nameof(key));
var cacheKey = string.Concat(key, date);
var cacheKey = string.Concat(appId, date);
return Cache.GetOrCreateAsync(cacheKey, entry =>
{
entry.AbsoluteExpirationRelativeToNow = CacheDuration;
return inner.GetMonthlyCallsAsync(key, date);
return inner.GetMonthlyCallsAsync(appId, date);
});
}
}

6
src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs

@ -13,10 +13,10 @@ namespace Squidex.Infrastructure.UsageTracking
{
public interface IUsageTracker
{
Task TrackAsync(string key, string category, double weight, double elapsedMs);
Task TrackAsync(Guid appId, string category, double weight, double elapsedMs);
Task<long> GetMonthlyCallsAsync(string key, DateTime date);
Task<long> GetMonthlyCallsAsync(Guid appId, DateTime date);
Task<IReadOnlyDictionary<string, IReadOnlyList<DateUsage>>> QueryAsync(string key, DateTime fromDate, DateTime toDate);
Task<IReadOnlyDictionary<string, IReadOnlyList<DateUsage>>> QueryAsync(Guid appId, DateTime fromDate, DateTime toDate);
}
}

4
src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs

@ -58,7 +58,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics
[ApiCosts(0)]
public async Task<IActionResult> GetMonthlyCalls(string app)
{
var count = await usageTracker.GetMonthlyCallsAsync(AppId.ToString(), DateTime.Today);
var count = await usageTracker.GetMonthlyCallsAsync(AppId, DateTime.Today);
var plan = appPlanProvider.GetPlanForApp(App);
@ -90,7 +90,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics
return BadRequest();
}
var entities = await usageTracker.QueryAsync(AppId.ToString(), fromDate.Date, toDate.Date);
var entities = await usageTracker.QueryAsync(AppId, fromDate.Date, toDate.Date);
var response = entities.ToDictionary(x => x.Key, x => x.Value.Select(CallsUsageDto.FromUsage).ToList());

4
src/Squidex/Pipeline/ApiCostsFilter.cs

@ -53,7 +53,7 @@ namespace Squidex.Pipeline
{
var plan = appPlanProvider.GetPlanForApp(appFeature.App);
var usage = await usageTracker.GetMonthlyCallsAsync(appFeature.App.Id.ToString(), DateTime.Today);
var usage = await usageTracker.GetMonthlyCallsAsync(appFeature.App.Id, DateTime.Today);
if (plan.MaxApiCalls >= 0 && usage > plan.MaxApiCalls * 1.1)
{
@ -72,7 +72,7 @@ namespace Squidex.Pipeline
{
var elapsedMs = watch.Stop();
await usageTracker.TrackAsync(appFeature.App.Id.ToString(), context.HttpContext.User.OpenIdClientId(), FilterDefinition.Weight, elapsedMs);
await usageTracker.TrackAsync(appFeature.App.Id, context.HttpContext.User.OpenIdClientId(), FilterDefinition.Weight, elapsedMs);
}
}
else

49
tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs

@ -19,6 +19,7 @@ namespace Squidex.Infrastructure.UsageTracking
{
private readonly IUsageRepository usageStore = A.Fake<IUsageRepository>();
private readonly ISemanticLog log = A.Fake<ISemanticLog>();
private readonly Guid appId = Guid.NewGuid();
private readonly BackgroundUsageTracker sut;
public BackgroundUsageTrackerTests()
@ -31,7 +32,7 @@ namespace Squidex.Infrastructure.UsageTracking
{
sut.Dispose();
return Assert.ThrowsAsync<ObjectDisposedException>(() => sut.TrackAsync("MyKey1", "category1", 1, 1000));
return Assert.ThrowsAsync<ObjectDisposedException>(() => sut.TrackAsync(appId, "category1", 1, 1000));
}
[Fact]
@ -39,7 +40,7 @@ namespace Squidex.Infrastructure.UsageTracking
{
sut.Dispose();
return Assert.ThrowsAsync<ObjectDisposedException>(() => sut.QueryAsync("MyKey1", DateTime.Today, DateTime.Today.AddDays(1)));
return Assert.ThrowsAsync<ObjectDisposedException>(() => sut.QueryAsync(appId, DateTime.Today, DateTime.Today.AddDays(1)));
}
[Fact]
@ -47,7 +48,7 @@ namespace Squidex.Infrastructure.UsageTracking
{
sut.Dispose();
return Assert.ThrowsAsync<ObjectDisposedException>(() => sut.GetMonthlyCallsAsync("MyKey1", DateTime.Today));
return Assert.ThrowsAsync<ObjectDisposedException>(() => sut.GetMonthlyCallsAsync(appId, DateTime.Today));
}
[Fact]
@ -63,10 +64,10 @@ namespace Squidex.Infrastructure.UsageTracking
new StoredUsage("category1", date.AddDays(7), Counters(17, 22))
};
A.CallTo(() => usageStore.QueryAsync("MyKey1", new DateTime(2016, 1, 1), new DateTime(2016, 1, 31)))
A.CallTo(() => usageStore.QueryAsync($"{appId}_API", new DateTime(2016, 1, 1), new DateTime(2016, 1, 31)))
.Returns(originalData);
var result = await sut.GetMonthlyCallsAsync("MyKey1", date);
var result = await sut.GetMonthlyCallsAsync(appId, date);
Assert.Equal(55, result);
}
@ -86,10 +87,10 @@ namespace Squidex.Infrastructure.UsageTracking
new StoredUsage(null, f.AddDays(2), Counters(11, 14))
};
A.CallTo(() => usageStore.QueryAsync("MyKey1", f, t))
A.CallTo(() => usageStore.QueryAsync($"{appId}_API", f, t))
.Returns(originalData);
var result = await sut.QueryAsync("MyKey1", f, t);
var result = await sut.QueryAsync(appId, f, t);
var expected = new Dictionary<string, List<DateUsage>>
{
@ -120,10 +121,10 @@ namespace Squidex.Infrastructure.UsageTracking
var f = DateTime.Today;
var t = DateTime.Today.AddDays(4);
A.CallTo(() => usageStore.QueryAsync("MyKey1", f, t))
A.CallTo(() => usageStore.QueryAsync($"{appId}_API", f, t))
.Returns(new List<StoredUsage>());
var result = await sut.QueryAsync("MyKey1", f, t);
var result = await sut.QueryAsync(appId, f, t);
var expected = new Dictionary<string, List<DateUsage>>
{
@ -143,8 +144,8 @@ namespace Squidex.Infrastructure.UsageTracking
[Fact]
public async Task Should_not_track_if_weight_less_than_zero()
{
await sut.TrackAsync("MyKey1", "MyCategory", -1, 1000);
await sut.TrackAsync("MyKey1", "MyCategory", 0, 1000);
await sut.TrackAsync(appId, "MyCategory", -1, 1000);
await sut.TrackAsync(appId, "MyCategory", 0, 1000);
sut.Next();
sut.Dispose();
@ -156,18 +157,22 @@ namespace Squidex.Infrastructure.UsageTracking
[Fact]
public async Task Should_aggregate_and_store_on_dispose()
{
var appId1 = Guid.NewGuid();
var appId2 = Guid.NewGuid();
var appId3 = Guid.NewGuid();
var today = DateTime.Today;
await sut.TrackAsync("MyKey1", "MyCategory1", 1, 1000);
await sut.TrackAsync(appId1, "MyCategory1", 1, 1000);
await sut.TrackAsync("MyKey2", "MyCategory1", 1.0, 2000);
await sut.TrackAsync("MyKey2", "MyCategory1", 0.5, 3000);
await sut.TrackAsync(appId2, "MyCategory1", 1.0, 2000);
await sut.TrackAsync(appId2, "MyCategory1", 0.5, 3000);
await sut.TrackAsync("MyKey3", "MyCategory1", 0.3, 4000);
await sut.TrackAsync("MyKey3", "MyCategory1", 0.1, 5000);
await sut.TrackAsync(appId3, "MyCategory1", 0.3, 4000);
await sut.TrackAsync(appId3, "MyCategory1", 0.1, 5000);
await sut.TrackAsync("MyKey3", null, 0.5, 2000);
await sut.TrackAsync("MyKey3", null, 0.5, 6000);
await sut.TrackAsync(appId3, null, 0.5, 2000);
await sut.TrackAsync(appId3, null, 0.5, 6000);
UsageUpdate[] updates = null;
@ -179,10 +184,10 @@ namespace Squidex.Infrastructure.UsageTracking
updates.Should().BeEquivalentTo(new[]
{
new UsageUpdate(today, "MyKey1", "MyCategory1", Counters(1.0, 1000)),
new UsageUpdate(today, "MyKey2", "MyCategory1", Counters(1.5, 5000)),
new UsageUpdate(today, "MyKey3", "MyCategory1", Counters(0.4, 9000)),
new UsageUpdate(today, "MyKey3", "*", Counters(1, 8000))
new UsageUpdate(today, $"{appId1}_API", "MyCategory1", Counters(1.0, 1000)),
new UsageUpdate(today, $"{appId2}_API", "MyCategory1", Counters(1.5, 5000)),
new UsageUpdate(today, $"{appId3}_API", "MyCategory1", Counters(0.4, 9000)),
new UsageUpdate(today, $"{appId3}_API", "*", Counters(1, 8000))
}, o => o.ComparingByMembers<UsageUpdate>());
A.CallTo(() => usageStore.TrackUsagesAsync(A<UsageUpdate[]>.Ignored))

21
tests/Squidex.Infrastructure.Tests/UsageTracking/ThreadingUsageTrackerTests.cs → tests/Squidex.Infrastructure.Tests/UsageTracking/CachingUsageTrackerTests.cs

@ -14,13 +14,14 @@ using Xunit;
namespace Squidex.Infrastructure.UsageTracking
{
public class ThreadingUsageTrackerTests
public class CachingUsageTrackerTests
{
private readonly MemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
private readonly Guid appId = Guid.NewGuid();
private readonly IUsageTracker inner = A.Fake<IUsageTracker>();
private readonly IUsageTracker sut;
public ThreadingUsageTrackerTests()
public CachingUsageTrackerTests()
{
sut = new CachingUsageTracker(inner, cache);
}
@ -28,34 +29,34 @@ namespace Squidex.Infrastructure.UsageTracking
[Fact]
public async Task Should_forward_track_call()
{
await sut.TrackAsync("MyKey", "MyCategory", 123, 456);
await sut.TrackAsync(appId, "MyCategory", 123, 456);
A.CallTo(() => inner.TrackAsync("MyKey", "MyCategory", 123, 456))
A.CallTo(() => inner.TrackAsync(appId, "MyCategory", 123, 456))
.MustHaveHappened();
}
[Fact]
public async Task Should_forward_query_call()
{
await sut.QueryAsync("MyKey", DateTime.MaxValue, DateTime.MinValue);
await sut.QueryAsync(appId, DateTime.MaxValue, DateTime.MinValue);
A.CallTo(() => inner.QueryAsync("MyKey", DateTime.MaxValue, DateTime.MinValue))
A.CallTo(() => inner.QueryAsync(appId, DateTime.MaxValue, DateTime.MinValue))
.MustHaveHappened();
}
[Fact]
public async Task Should_cache_monthly_usage()
{
A.CallTo(() => inner.GetMonthlyCallsAsync("MyKey", DateTime.Today))
A.CallTo(() => inner.GetMonthlyCallsAsync(appId, DateTime.Today))
.Returns(100);
var result1 = await sut.GetMonthlyCallsAsync("MyKey", DateTime.Today);
var result2 = await sut.GetMonthlyCallsAsync("MyKey", DateTime.Today);
var result1 = await sut.GetMonthlyCallsAsync(appId, DateTime.Today);
var result2 = await sut.GetMonthlyCallsAsync(appId, DateTime.Today);
Assert.Equal(100, result1);
Assert.Equal(100, result2);
A.CallTo(() => inner.GetMonthlyCallsAsync("MyKey", DateTime.Today))
A.CallTo(() => inner.GetMonthlyCallsAsync(appId, DateTime.Today))
.MustHaveHappened(Repeated.Exactly.Once);
}
}

12
tests/Squidex.Tests/Pipeline/ApiCostsFilterTests.cs

@ -57,7 +57,7 @@ namespace Squidex.Pipeline
A.CallTo(() => appPlan.MaxApiCalls)
.ReturnsLazily(x => apiCallsMax);
A.CallTo(() => usageTracker.GetMonthlyCallsAsync(A<string>.Ignored, DateTime.Today))
A.CallTo(() => usageTracker.GetMonthlyCallsAsync(A<Guid>.Ignored, DateTime.Today))
.ReturnsLazily(x => Task.FromResult(apiCallsCurrent));
next = () =>
@ -85,7 +85,7 @@ namespace Squidex.Pipeline
Assert.Equal(429, (actionContext.Result as StatusCodeResult).StatusCode);
Assert.False(isNextCalled);
A.CallTo(() => usageTracker.TrackAsync(A<string>.Ignored, A<string>.Ignored, A<double>.Ignored, A<double>.Ignored))
A.CallTo(() => usageTracker.TrackAsync(A<Guid>.Ignored, A<string>.Ignored, A<double>.Ignored, A<double>.Ignored))
.MustNotHaveHappened();
}
@ -103,7 +103,7 @@ namespace Squidex.Pipeline
Assert.True(isNextCalled);
A.CallTo(() => usageTracker.TrackAsync(A<string>.Ignored, A<string>.Ignored, 13, A<double>.Ignored))
A.CallTo(() => usageTracker.TrackAsync(A<Guid>.Ignored, A<string>.Ignored, 13, A<double>.Ignored))
.MustHaveHappened();
}
@ -121,7 +121,7 @@ namespace Squidex.Pipeline
Assert.True(isNextCalled);
A.CallTo(() => usageTracker.TrackAsync(A<string>.Ignored, A<string>.Ignored, 13, A<double>.Ignored))
A.CallTo(() => usageTracker.TrackAsync(A<Guid>.Ignored, A<string>.Ignored, 13, A<double>.Ignored))
.MustHaveHappened();
}
@ -139,7 +139,7 @@ namespace Squidex.Pipeline
Assert.True(isNextCalled);
A.CallTo(() => usageTracker.TrackAsync(A<string>.Ignored, A<string>.Ignored, A<double>.Ignored, A<double>.Ignored))
A.CallTo(() => usageTracker.TrackAsync(A<Guid>.Ignored, A<string>.Ignored, A<double>.Ignored, A<double>.Ignored))
.MustNotHaveHappened();
}
@ -155,7 +155,7 @@ namespace Squidex.Pipeline
Assert.True(isNextCalled);
A.CallTo(() => usageTracker.TrackAsync(A<string>.Ignored, A<string>.Ignored, A<double>.Ignored, A<double>.Ignored))
A.CallTo(() => usageTracker.TrackAsync(A<Guid>.Ignored, A<string>.Ignored, A<double>.Ignored, A<double>.Ignored))
.MustNotHaveHappened();
}

Loading…
Cancel
Save