mirror of https://github.com/Squidex/squidex.git
27 changed files with 533 additions and 128 deletions
@ -0,0 +1,24 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Rules.Triggers |
|||
{ |
|||
[TypeName(nameof(UsageTrigger))] |
|||
public sealed class UsageTrigger : RuleTrigger |
|||
{ |
|||
public const string Name = "Usage"; |
|||
|
|||
public int Limit { get; set; } |
|||
|
|||
public override T Accept<T>(IRuleTriggerVisitor<T> visitor) |
|||
{ |
|||
return visitor.Visit(this); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents |
|||
{ |
|||
public sealed class EnrichedUsageExceededEvent : EnrichedEvent |
|||
{ |
|||
public long Current { get; set; } |
|||
|
|||
public long Limit { get; set; } |
|||
|
|||
public override long Partition |
|||
{ |
|||
get { return AppId.GetHashCode(); } |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Runtime.Serialization; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Shared.Users; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents |
|||
{ |
|||
public abstract class EnrichedUserEvent : EnrichedEvent |
|||
{ |
|||
public RefToken Actor { get; set; } |
|||
|
|||
[IgnoreDataMember] |
|||
public IUser User { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Orleans; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking |
|||
{ |
|||
public interface IUsageTrackerGrain : IGrainWithStringKey |
|||
{ |
|||
Task AddTargetAsync(NamedId<Guid> appId, int limits); |
|||
|
|||
Task ActivateTargetAsync(NamedId<Guid> appId); |
|||
|
|||
Task DeactivateTargetAsync(NamedId<Guid> appId); |
|||
|
|||
Task RemoveTargetAsync(NamedId<Guid> appId); |
|||
} |
|||
} |
|||
@ -0,0 +1,143 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Orleans; |
|||
using Orleans.Runtime; |
|||
using Squidex.Domain.Apps.Events; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.EventSourcing; |
|||
using Squidex.Infrastructure.Orleans; |
|||
using Squidex.Infrastructure.States; |
|||
using Squidex.Infrastructure.UsageTracking; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking |
|||
{ |
|||
public sealed class UsageTrackerGrain : GrainOfString, IRemindable, IUsageTrackerGrain |
|||
{ |
|||
private readonly IStore<string> store; |
|||
private readonly IUsageTracker usageTracker; |
|||
private IPersistence<State> persistence; |
|||
private State state; |
|||
|
|||
public sealed class Target |
|||
{ |
|||
public int Limit { get; set; } |
|||
|
|||
public bool Enabled { get; set; } |
|||
|
|||
public DateTime Triggered { get; set; } |
|||
} |
|||
|
|||
[CollectionName("UsageTracker")] |
|||
public sealed class State |
|||
{ |
|||
public Dictionary<NamedId<Guid>, Target> Targets { get; set; } = new Dictionary<NamedId<Guid>, Target>(); |
|||
} |
|||
|
|||
public UsageTrackerGrain(IStore<string> store, IUsageTracker usageTracker) |
|||
{ |
|||
Guard.NotNull(store, nameof(store)); |
|||
Guard.NotNull(usageTracker, nameof(usageTracker)); |
|||
|
|||
this.store = store; |
|||
|
|||
this.usageTracker = usageTracker; |
|||
} |
|||
|
|||
public override Task OnActivateAsync(string key) |
|||
{ |
|||
DelayDeactivation(TimeSpan.FromDays(1)); |
|||
|
|||
RegisterOrUpdateReminder("Default", TimeSpan.Zero, TimeSpan.FromMinutes(10)); |
|||
|
|||
persistence = store.WithSnapshotsAndEventSourcing<State, string>(GetType(), key, ApplySnapshot, ApplyEvent); |
|||
|
|||
return persistence.ReadAsync(); |
|||
} |
|||
|
|||
private void ApplySnapshot(State s) |
|||
{ |
|||
state = s; |
|||
} |
|||
|
|||
private void ApplyEvent(Envelope<IEvent> @event) |
|||
{ |
|||
} |
|||
|
|||
public async Task ReceiveReminder(string reminderName, TickStatus status) |
|||
{ |
|||
var today = DateTime.Today; |
|||
|
|||
foreach (var kvp in state.Targets) |
|||
{ |
|||
var appId = kvp.Key; |
|||
|
|||
if (!IsSameMonth(today, kvp.Value.Triggered)) |
|||
{ |
|||
var usage = await usageTracker.GetMonthlyCallsAsync(appId.Id.ToString(), today); |
|||
|
|||
var limit = kvp.Value.Limit; |
|||
|
|||
if (usage > limit) |
|||
{ |
|||
kvp.Value.Triggered = today; |
|||
|
|||
var @event = new AppUsageExceeded { AppId = appId, Current = usage, Limit = limit }; |
|||
|
|||
await persistence.WriteEventsAsync(new[] |
|||
{ |
|||
Envelope.Create(@event) |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
|
|||
await persistence.WriteSnapshotAsync(state); |
|||
} |
|||
|
|||
private static bool IsSameMonth(DateTime lhs, DateTime rhs) |
|||
{ |
|||
return lhs.Year == rhs.Year && lhs.Month == rhs.Month; |
|||
} |
|||
|
|||
public Task ActivateTargetAsync(NamedId<Guid> appId) |
|||
{ |
|||
UpdateTarget(appId, t => t.Enabled = true); |
|||
|
|||
return persistence.WriteSnapshotAsync(state); |
|||
} |
|||
|
|||
public Task DeactivateTargetAsync(NamedId<Guid> appId) |
|||
{ |
|||
UpdateTarget(appId, t => t.Enabled = false); |
|||
|
|||
return persistence.WriteSnapshotAsync(state); |
|||
} |
|||
|
|||
public Task AddTargetAsync(NamedId<Guid> appId, int limits) |
|||
{ |
|||
UpdateTarget(appId, t => t.Limit = limits); |
|||
|
|||
return persistence.WriteSnapshotAsync(state); |
|||
} |
|||
|
|||
public Task RemoveTargetAsync(NamedId<Guid> appId) |
|||
{ |
|||
state.Targets.Remove(appId); |
|||
|
|||
return persistence.WriteSnapshotAsync(state); |
|||
} |
|||
|
|||
private void UpdateTarget(NamedId<Guid> appId, Action<Target> updater) |
|||
{ |
|||
updater(state.Targets.GetOrAddNew(appId));; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,69 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Threading.Tasks; |
|||
using Orleans; |
|||
using Squidex.Domain.Apps.Core.HandleRules; |
|||
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; |
|||
using Squidex.Domain.Apps.Core.Rules.Triggers; |
|||
using Squidex.Domain.Apps.Events; |
|||
using Squidex.Domain.Apps.Events.Rules; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Orleans; |
|||
using Squidex.Infrastructure.Tasks; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking |
|||
{ |
|||
public sealed class UsageTriggerHandler : RuleTriggerHandler<UsageTrigger, AppEvent, EnrichedUsageExceededEvent> |
|||
{ |
|||
private readonly IUsageTrackerGrain usageTrackerGrain; |
|||
|
|||
public UsageTriggerHandler(IGrainFactory grainFactory) |
|||
{ |
|||
Guard.NotNull(grainFactory, nameof(grainFactory)); |
|||
|
|||
usageTrackerGrain = grainFactory.GetGrain<IUsageTrackerGrain>(SingleGrain.Id); |
|||
} |
|||
|
|||
protected override async Task<bool> TriggersAsync(AppEvent @event, UsageTrigger trigger) |
|||
{ |
|||
switch (@event) |
|||
{ |
|||
case RuleDeleted _: |
|||
await usageTrackerGrain.RemoveTargetAsync(@event.AppId); |
|||
break; |
|||
case RuleEnabled _: |
|||
await usageTrackerGrain.ActivateTargetAsync(@event.AppId); |
|||
break; |
|||
case RuleDisabled _: |
|||
await usageTrackerGrain.DeactivateTargetAsync(@event.AppId); |
|||
break; |
|||
case RuleCreated ruleCreated: |
|||
if (ruleCreated.Trigger is UsageTrigger createdTrigger) |
|||
{ |
|||
await usageTrackerGrain.AddTargetAsync(ruleCreated.AppId, createdTrigger.Limit); |
|||
} |
|||
|
|||
break; |
|||
case RuleUpdated ruleUpdated: |
|||
if (ruleUpdated.Trigger is UsageTrigger updatedTrigger) |
|||
{ |
|||
await usageTrackerGrain.AddTargetAsync(ruleUpdated.AppId, updatedTrigger.Limit); |
|||
} |
|||
|
|||
break; |
|||
} |
|||
|
|||
return @event is AppUsageExceeded; |
|||
} |
|||
|
|||
protected override Task<bool> TriggersAsync(EnrichedUsageExceededEvent @event, UsageTrigger trigger) |
|||
{ |
|||
return TaskHelper.True; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure.EventSourcing; |
|||
|
|||
namespace Squidex.Domain.Apps.Events |
|||
{ |
|||
[EventType(nameof(AppUsageExceeded))] |
|||
public sealed class AppUsageExceeded : AppEvent |
|||
{ |
|||
public long Current { get; set; } |
|||
|
|||
public long Limit { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Domain.Apps.Core.Rules; |
|||
using Squidex.Domain.Apps.Core.Rules.Triggers; |
|||
using Squidex.Infrastructure.Reflection; |
|||
|
|||
namespace Squidex.Areas.Api.Controllers.Rules.Models.Triggers |
|||
{ |
|||
public sealed class UsageTriggerDto : RuleTriggerDto |
|||
{ |
|||
/// <summary>
|
|||
/// The number of monthly api calls.
|
|||
/// </summary>
|
|||
public int Limit { get; set; } |
|||
|
|||
public override RuleTrigger ToTrigger() |
|||
{ |
|||
return SimpleMapper.Map(this, new UsageTrigger()); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue