mirror of https://github.com/Squidex/squidex.git
47 changed files with 1303 additions and 1617 deletions
@ -0,0 +1,91 @@ |
|||||
|
// ==========================================================================
|
||||
|
// EventConsumerActorManager.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Infrastructure.CQRS.Events.Actors.Messages; |
||||
|
using Squidex.Infrastructure.States; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.CQRS.Events.Actors |
||||
|
{ |
||||
|
public sealed class EventConsumerActorManager : DisposableObjectBase, IExternalSystem |
||||
|
{ |
||||
|
private readonly IStateFactory factory; |
||||
|
private readonly IPubSub pubSub; |
||||
|
private readonly List<IEventConsumer> consumers; |
||||
|
private readonly List<IDisposable> subscriptions = new List<IDisposable>(); |
||||
|
|
||||
|
public EventConsumerActorManager(IEnumerable<IEventConsumer> consumers, IPubSub pubSub, IStateFactory factory) |
||||
|
{ |
||||
|
Guard.NotNull(pubSub, nameof(pubSub)); |
||||
|
Guard.NotNull(factory, nameof(factory)); |
||||
|
Guard.NotNull(consumers, nameof(consumers)); |
||||
|
|
||||
|
this.pubSub = pubSub; |
||||
|
this.factory = factory; |
||||
|
this.consumers = consumers.ToList(); |
||||
|
} |
||||
|
|
||||
|
public void Connect() |
||||
|
{ |
||||
|
var actors = new Dictionary<string, EventConsumerActor>(); |
||||
|
|
||||
|
foreach (var consumer in consumers) |
||||
|
{ |
||||
|
var actor = factory.GetAsync<EventConsumerActor, EventConsumerState>(consumer.Name).Result; |
||||
|
|
||||
|
actors[consumer.Name] = actor; |
||||
|
actor.Activate(consumer); |
||||
|
} |
||||
|
|
||||
|
subscriptions.Add(pubSub.Subscribe<StartConsumerMessage>(m => |
||||
|
{ |
||||
|
if (actors.TryGetValue(m.ConsumerName, out var actor)) |
||||
|
{ |
||||
|
actor.Start(); |
||||
|
} |
||||
|
})); |
||||
|
|
||||
|
subscriptions.Add(pubSub.Subscribe<StopConsumerMessage>(m => |
||||
|
{ |
||||
|
if (actors.TryGetValue(m.ConsumerName, out var actor)) |
||||
|
{ |
||||
|
actor.Stop(); |
||||
|
} |
||||
|
})); |
||||
|
|
||||
|
subscriptions.Add(pubSub.Subscribe<ResetConsumerMessage>(m => |
||||
|
{ |
||||
|
if (actors.TryGetValue(m.ConsumerName, out var actor)) |
||||
|
{ |
||||
|
actor.Reset(); |
||||
|
} |
||||
|
})); |
||||
|
|
||||
|
subscriptions.Add(pubSub.ReceiveAsync<GetStatesRequest, GetStatesResponse>(request => |
||||
|
{ |
||||
|
var states = actors.Values.Select(x => x.GetState()).ToArray(); |
||||
|
|
||||
|
return Task.FromResult(new GetStatesResponse { States = states }); |
||||
|
})); |
||||
|
} |
||||
|
|
||||
|
protected override void DisposeObject(bool disposing) |
||||
|
{ |
||||
|
if (disposing) |
||||
|
{ |
||||
|
foreach (var subscription in subscriptions) |
||||
|
{ |
||||
|
subscription.Dispose(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,52 @@ |
|||||
|
// ==========================================================================
|
||||
|
// EventConsumerGrainState.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using Squidex.Infrastructure.Reflection; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.CQRS.Events.Actors |
||||
|
{ |
||||
|
public sealed class EventConsumerState |
||||
|
{ |
||||
|
public bool IsStopped { get; set; } |
||||
|
|
||||
|
public string Error { get; set; } |
||||
|
|
||||
|
public string Position { get; set; } |
||||
|
|
||||
|
public EventConsumerState Reset() |
||||
|
{ |
||||
|
return new EventConsumerState(); |
||||
|
} |
||||
|
|
||||
|
public EventConsumerState Handled(string position) |
||||
|
{ |
||||
|
return new EventConsumerState { Position = position }; |
||||
|
} |
||||
|
|
||||
|
public EventConsumerState Failed(Exception ex) |
||||
|
{ |
||||
|
return new EventConsumerState { Position = Position, IsStopped = true, Error = ex?.ToString() }; |
||||
|
} |
||||
|
|
||||
|
public EventConsumerState Stopped() |
||||
|
{ |
||||
|
return new EventConsumerState { Position = Position, IsStopped = true }; |
||||
|
} |
||||
|
|
||||
|
public EventConsumerState Started() |
||||
|
{ |
||||
|
return new EventConsumerState { Position = Position, IsStopped = false }; |
||||
|
} |
||||
|
|
||||
|
public EventConsumerInfo ToInfo(string name) |
||||
|
{ |
||||
|
return SimpleMapper.Map(this, new EventConsumerInfo { Name = name }); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,17 +1,14 @@ |
|||||
// ==========================================================================
|
// ==========================================================================
|
||||
// IJsonValue.cs
|
// GetStatesRequest.cs
|
||||
// Squidex Headless CMS
|
// Squidex Headless CMS
|
||||
// ==========================================================================
|
// ==========================================================================
|
||||
// Copyright (c) Squidex Group
|
// Copyright (c) Squidex Group
|
||||
// All rights reserved.
|
// All rights reserved.
|
||||
// ==========================================================================
|
// ==========================================================================
|
||||
|
|
||||
namespace Squidex.Infrastructure.Json.Orleans |
namespace Squidex.Infrastructure.CQRS.Events.Actors.Messages |
||||
{ |
{ |
||||
public interface IJsonValue |
public sealed class GetStatesRequest |
||||
{ |
{ |
||||
object Value { get; } |
|
||||
|
|
||||
bool IsImmutable { get; } |
|
||||
} |
} |
||||
} |
} |
||||
@ -1,18 +1,15 @@ |
|||||
// ==========================================================================
|
// ==========================================================================
|
||||
// JExtensions.cs
|
// GetStatesResponse.cs
|
||||
// Squidex Headless CMS
|
// Squidex Headless CMS
|
||||
// ==========================================================================
|
// ==========================================================================
|
||||
// Copyright (c) Squidex Group
|
// Copyright (c) Squidex Group
|
||||
// All rights reserved.
|
// All rights reserved.
|
||||
// ==========================================================================
|
// ==========================================================================
|
||||
|
|
||||
namespace Squidex.Infrastructure.Json.Orleans |
namespace Squidex.Infrastructure.CQRS.Events.Actors.Messages |
||||
{ |
{ |
||||
public static class JExtensions |
public sealed class GetStatesResponse |
||||
{ |
{ |
||||
public static J<T> AsJ<T>(this T value, bool immutable = true) |
public EventConsumerInfo[] States { get; set; } |
||||
{ |
|
||||
return new J<T>(value, immutable); |
|
||||
} |
|
||||
} |
} |
||||
} |
} |
||||
@ -0,0 +1,15 @@ |
|||||
|
// ==========================================================================
|
||||
|
// ResetConsumerMessage.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Infrastructure.CQRS.Events.Actors.Messages |
||||
|
{ |
||||
|
public sealed class ResetConsumerMessage |
||||
|
{ |
||||
|
public string ConsumerName { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,15 @@ |
|||||
|
// ==========================================================================
|
||||
|
// StartConsumerMessage.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Infrastructure.CQRS.Events.Actors.Messages |
||||
|
{ |
||||
|
public sealed class StartConsumerMessage |
||||
|
{ |
||||
|
public string ConsumerName { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,15 @@ |
|||||
|
// ==========================================================================
|
||||
|
// StopConsumerMessage.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Infrastructure.CQRS.Events.Actors.Messages |
||||
|
{ |
||||
|
public sealed class StopConsumerMessage |
||||
|
{ |
||||
|
public string ConsumerName { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -1,31 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// EventConsumerBootstrap.cs
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex Group
|
|
||||
// All rights reserved.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System.Threading.Tasks; |
|
||||
using Orleans.Providers; |
|
||||
using Squidex.Infrastructure.Tasks; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.CQRS.Events.Orleans.Grains |
|
||||
{ |
|
||||
public sealed class EventConsumerBootstrap : IBootstrapProvider |
|
||||
{ |
|
||||
public string Name { get; private set; } |
|
||||
|
|
||||
public Task Close() |
|
||||
{ |
|
||||
return TaskHelper.Done; |
|
||||
} |
|
||||
|
|
||||
public Task Init(string name, IProviderRuntime providerRuntime, IProviderConfiguration config) |
|
||||
{ |
|
||||
Name = name; |
|
||||
|
|
||||
return providerRuntime.GrainFactory.GetGrain<IEventConsumerRegistryGrain>("Default").ActivateAsync(null); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,34 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// IEventConsumerGrain.cs
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex Group
|
|
||||
// All rights reserved.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.Threading.Tasks; |
|
||||
using Orleans; |
|
||||
using Orleans.Concurrency; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.CQRS.Events.Orleans.Grains |
|
||||
{ |
|
||||
public interface IEventConsumerGrain : IGrainWithStringKey |
|
||||
{ |
|
||||
Task<Immutable<EventConsumerInfo>> GetStateAsync(); |
|
||||
|
|
||||
Task ActivateAsync(); |
|
||||
|
|
||||
Task StopAsync(); |
|
||||
|
|
||||
Task StartAsync(); |
|
||||
|
|
||||
Task ResetAsync(); |
|
||||
|
|
||||
Task OnEventAsync(Immutable<IEventSubscription> subscription, Immutable<StoredEvent> storedEvent); |
|
||||
|
|
||||
Task OnErrorAsync(Immutable<IEventSubscription> subscription, Immutable<Exception> exception); |
|
||||
|
|
||||
Task OnClosedAsync(Immutable<IEventSubscription> subscription); |
|
||||
} |
|
||||
} |
|
||||
@ -1,28 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// IEventConsumerRegistryGrain.cs
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex Group
|
|
||||
// All rights reserved.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System.Collections.Generic; |
|
||||
using System.Threading.Tasks; |
|
||||
using Orleans; |
|
||||
using Orleans.Concurrency; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.CQRS.Events.Orleans.Grains |
|
||||
{ |
|
||||
public interface IEventConsumerRegistryGrain : IGrainWithStringKey |
|
||||
{ |
|
||||
Task ActivateAsync(string streamName); |
|
||||
|
|
||||
Task StopAsync(string consumerName); |
|
||||
|
|
||||
Task StartAsync(string consumerName); |
|
||||
|
|
||||
Task ResetAsync(string consumerName); |
|
||||
|
|
||||
Task<Immutable<List<EventConsumerInfo>>> GetConsumersAsync(); |
|
||||
} |
|
||||
} |
|
||||
@ -1,52 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// EventConsumerGrainState.cs
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex Group
|
|
||||
// All rights reserved.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using Squidex.Infrastructure.Reflection; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.CQRS.Events.Orleans.Grains.Implementation |
|
||||
{ |
|
||||
public sealed class EventConsumerGrainState |
|
||||
{ |
|
||||
public bool IsStopped { get; set; } |
|
||||
|
|
||||
public string Error { get; set; } |
|
||||
|
|
||||
public string Position { get; set; } |
|
||||
|
|
||||
public static EventConsumerGrainState Initial() |
|
||||
{ |
|
||||
return new EventConsumerGrainState(); |
|
||||
} |
|
||||
|
|
||||
public static EventConsumerGrainState Handled(string position) |
|
||||
{ |
|
||||
return new EventConsumerGrainState { Position = position }; |
|
||||
} |
|
||||
|
|
||||
public EventConsumerGrainState Failed(Exception ex) |
|
||||
{ |
|
||||
return new EventConsumerGrainState { Position = Position, IsStopped = true, Error = ex?.ToString() }; |
|
||||
} |
|
||||
|
|
||||
public EventConsumerGrainState Stopped() |
|
||||
{ |
|
||||
return new EventConsumerGrainState { Position = Position, IsStopped = true }; |
|
||||
} |
|
||||
|
|
||||
public EventConsumerGrainState Started() |
|
||||
{ |
|
||||
return new EventConsumerGrainState { Position = Position, IsStopped = false }; |
|
||||
} |
|
||||
|
|
||||
public EventConsumerInfo ToInfo(string name) |
|
||||
{ |
|
||||
return SimpleMapper.Map(this, new EventConsumerInfo { Name = name }); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,98 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// EventConsumerRegistryGrain.cs
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex Group
|
|
||||
// All rights reserved.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Linq; |
|
||||
using System.Text.RegularExpressions; |
|
||||
using System.Threading.Tasks; |
|
||||
using Orleans; |
|
||||
using Orleans.Concurrency; |
|
||||
using Orleans.Core; |
|
||||
using Orleans.Runtime; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.CQRS.Events.Orleans.Grains.Implementation |
|
||||
{ |
|
||||
public class EventConsumerRegistryGrain : Grain, IEventConsumerRegistryGrain, IRemindable |
|
||||
{ |
|
||||
private readonly IEnumerable<IEventConsumer> eventConsumers; |
|
||||
|
|
||||
public EventConsumerRegistryGrain(IEnumerable<IEventConsumer> eventConsumers) |
|
||||
: this(eventConsumers, null, null) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
protected EventConsumerRegistryGrain( |
|
||||
IEnumerable<IEventConsumer> eventConsumers, |
|
||||
IGrainIdentity identity, |
|
||||
IGrainRuntime runtime) |
|
||||
: base(identity, runtime) |
|
||||
{ |
|
||||
Guard.NotNull(eventConsumers, nameof(eventConsumers)); |
|
||||
|
|
||||
this.eventConsumers = eventConsumers; |
|
||||
} |
|
||||
|
|
||||
public Task ReceiveReminder(string reminderName, TickStatus status) |
|
||||
{ |
|
||||
return ActivateAsync(null); |
|
||||
} |
|
||||
|
|
||||
public override Task OnActivateAsync() |
|
||||
{ |
|
||||
DelayDeactivation(TimeSpan.FromDays(1)); |
|
||||
|
|
||||
RegisterOrUpdateReminder("Default", TimeSpan.Zero, TimeSpan.FromMinutes(10)); |
|
||||
RegisterTimer(x => ActivateAsync(null), null, TimeSpan.Zero, TimeSpan.FromSeconds(10)); |
|
||||
|
|
||||
return Task.FromResult(true); |
|
||||
} |
|
||||
|
|
||||
public Task ActivateAsync(string streamName) |
|
||||
{ |
|
||||
var tasks = |
|
||||
eventConsumers |
|
||||
.Where(c => streamName == null || Regex.IsMatch(streamName, c.EventsFilter)) |
|
||||
.Select(c => GrainFactory.GetGrain<IEventConsumerGrain>(c.Name)) |
|
||||
.Select(c => c.ActivateAsync()); |
|
||||
|
|
||||
return Task.WhenAll(tasks); |
|
||||
} |
|
||||
|
|
||||
public Task<Immutable<List<EventConsumerInfo>>> GetConsumersAsync() |
|
||||
{ |
|
||||
var tasks = |
|
||||
eventConsumers |
|
||||
.Select(c => GrainFactory.GetGrain<IEventConsumerGrain>(c.Name)) |
|
||||
.Select(c => c.GetStateAsync()); |
|
||||
|
|
||||
return Task.WhenAll(tasks).ContinueWith(x => new Immutable<List<EventConsumerInfo>>(x.Result.Select(r => r.Value).ToList())); |
|
||||
} |
|
||||
|
|
||||
public Task ResetAsync(string consumerName) |
|
||||
{ |
|
||||
var eventConsumer = GrainFactory.GetGrain<IEventConsumerGrain>(consumerName); |
|
||||
|
|
||||
return eventConsumer.ResetAsync(); |
|
||||
} |
|
||||
|
|
||||
public Task StartAsync(string consumerName) |
|
||||
{ |
|
||||
var eventConsumer = GrainFactory.GetGrain<IEventConsumerGrain>(consumerName); |
|
||||
|
|
||||
return eventConsumer.StartAsync(); |
|
||||
} |
|
||||
|
|
||||
public Task StopAsync(string consumerName) |
|
||||
{ |
|
||||
var eventConsumer = GrainFactory.GetGrain<IEventConsumerGrain>(consumerName); |
|
||||
|
|
||||
return eventConsumer.StopAsync(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,48 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// WrapperSubscription.cs
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex Group
|
|
||||
// All rights reserved.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.Threading; |
|
||||
using System.Threading.Tasks; |
|
||||
using Orleans.Concurrency; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.CQRS.Events.Orleans.Grains.Implementation |
|
||||
{ |
|
||||
internal sealed class WrapperSubscription : IEventSubscriber |
|
||||
{ |
|
||||
private readonly IEventConsumerGrain grain; |
|
||||
private readonly TaskScheduler scheduler; |
|
||||
|
|
||||
public WrapperSubscription(IEventConsumerGrain grain, TaskScheduler scheduler) |
|
||||
{ |
|
||||
this.grain = grain; |
|
||||
|
|
||||
this.scheduler = scheduler ?? TaskScheduler.Default; |
|
||||
} |
|
||||
|
|
||||
public Task OnEventAsync(IEventSubscription subscription, StoredEvent storedEvent) |
|
||||
{ |
|
||||
return Dispatch(() => grain.OnEventAsync(subscription.AsImmutable(), storedEvent.AsImmutable())); |
|
||||
} |
|
||||
|
|
||||
public Task OnErrorAsync(IEventSubscription subscription, Exception exception) |
|
||||
{ |
|
||||
return Dispatch(() => grain.OnErrorAsync(subscription.AsImmutable(), exception.AsImmutable())); |
|
||||
} |
|
||||
|
|
||||
public Task OnClosedAsync(IEventSubscription subscription) |
|
||||
{ |
|
||||
return Dispatch(() => grain.OnClosedAsync(subscription.AsImmutable())); |
|
||||
} |
|
||||
|
|
||||
private Task Dispatch(Func<Task> task) |
|
||||
{ |
|
||||
return Task<Task>.Factory.StartNew(() => task(), CancellationToken.None, TaskCreationOptions.None, scheduler).Unwrap(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,30 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// OrleansEventNotifier.cs
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex Group
|
|
||||
// All rights reserved.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using Orleans; |
|
||||
using Squidex.Infrastructure.CQRS.Events.Orleans.Grains; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.CQRS.Events.Orleans |
|
||||
{ |
|
||||
public sealed class OrleansEventNotifier : IEventNotifier |
|
||||
{ |
|
||||
private readonly IEventConsumerRegistryGrain eventConsumerRegistryGrain; |
|
||||
|
|
||||
public OrleansEventNotifier(IGrainFactory factory) |
|
||||
{ |
|
||||
Guard.NotNull(factory, nameof(factory)); |
|
||||
|
|
||||
eventConsumerRegistryGrain = factory.GetGrain<IEventConsumerRegistryGrain>("Default"); |
|
||||
} |
|
||||
|
|
||||
public void NotifyEventsStored(string streamName) |
|
||||
{ |
|
||||
eventConsumerRegistryGrain.ActivateAsync(streamName); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,15 @@ |
|||||
|
// ==========================================================================
|
||||
|
// InvalidationMessage.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Infrastructure.Caching |
||||
|
{ |
||||
|
public sealed class InvalidationMessage |
||||
|
{ |
||||
|
public string CacheKey { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -1,57 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// J.cs
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex Group
|
|
||||
// All rights reserved.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System.Threading.Tasks; |
|
||||
using Newtonsoft.Json; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.Json.Orleans |
|
||||
{ |
|
||||
public struct J<T> : IJsonValue |
|
||||
{ |
|
||||
private readonly T value; |
|
||||
private readonly bool isImmutable; |
|
||||
|
|
||||
public T Value |
|
||||
{ |
|
||||
get { return value; } |
|
||||
} |
|
||||
|
|
||||
bool IJsonValue.IsImmutable |
|
||||
{ |
|
||||
get { return isImmutable; } |
|
||||
} |
|
||||
|
|
||||
object IJsonValue.Value |
|
||||
{ |
|
||||
get { return Value; } |
|
||||
} |
|
||||
|
|
||||
[JsonConstructor] |
|
||||
public J(T value, bool isImmutable = false) |
|
||||
{ |
|
||||
this.value = value; |
|
||||
|
|
||||
this.isImmutable = isImmutable; |
|
||||
} |
|
||||
|
|
||||
public static implicit operator T(J<T> value) |
|
||||
{ |
|
||||
return value.Value; |
|
||||
} |
|
||||
|
|
||||
public static implicit operator J<T>(T d) |
|
||||
{ |
|
||||
return new J<T>(d); |
|
||||
} |
|
||||
|
|
||||
public static Task<J<T>> AsTask(T value) |
|
||||
{ |
|
||||
return Task.FromResult<J<T>>(value); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,91 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// JsonExternalSerializer.cs
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex Group
|
|
||||
// All rights reserved.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.IO; |
|
||||
using System.Linq; |
|
||||
using Newtonsoft.Json; |
|
||||
using Newtonsoft.Json.Linq; |
|
||||
using Orleans.Runtime; |
|
||||
using Orleans.Serialization; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.Json.Orleans |
|
||||
{ |
|
||||
public class JsonExternalSerializer : IExternalSerializer |
|
||||
{ |
|
||||
private readonly JsonSerializer serializer; |
|
||||
|
|
||||
public JsonExternalSerializer(JsonSerializer serializer) |
|
||||
{ |
|
||||
Guard.NotNull(serializer, nameof(serializer)); |
|
||||
|
|
||||
this.serializer = serializer; |
|
||||
} |
|
||||
|
|
||||
public void Initialize(Logger logger) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public bool IsSupportedType(Type itemType) |
|
||||
{ |
|
||||
return itemType.GetInterfaces().Contains(typeof(IJsonValue)); |
|
||||
} |
|
||||
|
|
||||
public object DeepCopy(object source, ICopyContext context) |
|
||||
{ |
|
||||
var jsonValue = source as IJsonValue; |
|
||||
|
|
||||
if (jsonValue == null) |
|
||||
{ |
|
||||
return null; |
|
||||
} |
|
||||
else if (jsonValue.IsImmutable) |
|
||||
{ |
|
||||
return jsonValue; |
|
||||
} |
|
||||
else if (jsonValue.Value == null) |
|
||||
{ |
|
||||
return jsonValue; |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
return JObject.FromObject(source, serializer).ToObject(source.GetType(), serializer); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public object Deserialize(Type expectedType, IDeserializationContext context) |
|
||||
{ |
|
||||
var outLength = context.StreamReader.ReadInt(); |
|
||||
var outBytes = context.StreamReader.ReadBytes(outLength); |
|
||||
|
|
||||
var stream = new MemoryStream(outBytes); |
|
||||
|
|
||||
using (var reader = new JsonTextReader(new StreamReader(stream))) |
|
||||
{ |
|
||||
return serializer.Deserialize(reader, expectedType); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public void Serialize(object item, ISerializationContext context, Type expectedType) |
|
||||
{ |
|
||||
var stream = new MemoryStream(); |
|
||||
|
|
||||
using (var writer = new JsonTextWriter(new StreamWriter(stream))) |
|
||||
{ |
|
||||
serializer.Serialize(writer, item); |
|
||||
|
|
||||
writer.Flush(); |
|
||||
} |
|
||||
|
|
||||
var outBytes = stream.ToArray(); |
|
||||
|
|
||||
context.StreamWriter.Write(outBytes.Length); |
|
||||
context.StreamWriter.Write(outBytes); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,83 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// GrainV2.cs
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex Group
|
|
||||
// All rights reserved.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System.Threading; |
|
||||
using System.Threading.Tasks; |
|
||||
using Orleans; |
|
||||
using Orleans.Core; |
|
||||
using Orleans.Runtime; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.Orleans |
|
||||
{ |
|
||||
public class GrainV2<TGrainState> : Grain where TGrainState : new() |
|
||||
{ |
|
||||
private readonly IGrainRuntime runtime; |
|
||||
private IStorage<TGrainState> storage; |
|
||||
|
|
||||
protected GrainV2(IGrainRuntime runtime) |
|
||||
{ |
|
||||
this.runtime = runtime; |
|
||||
} |
|
||||
|
|
||||
protected GrainV2(IGrainIdentity identity, IGrainRuntime runtime, IStorage<TGrainState> storage) |
|
||||
: base(identity, runtime) |
|
||||
{ |
|
||||
this.runtime = runtime; |
|
||||
this.storage = storage; |
|
||||
} |
|
||||
|
|
||||
protected TGrainState State |
|
||||
{ |
|
||||
get |
|
||||
{ |
|
||||
return storage.State; |
|
||||
} |
|
||||
set |
|
||||
{ |
|
||||
storage.State = value; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
protected virtual Task ClearStateAsync() |
|
||||
{ |
|
||||
return storage.ClearStateAsync(); |
|
||||
} |
|
||||
|
|
||||
protected virtual Task WriteStateAsync() |
|
||||
{ |
|
||||
return storage.WriteStateAsync(); |
|
||||
} |
|
||||
|
|
||||
protected virtual Task ReadStateAsync() |
|
||||
{ |
|
||||
return storage.ReadStateAsync(); |
|
||||
} |
|
||||
|
|
||||
public override void Participate(IGrainLifecycle lifecycle) |
|
||||
{ |
|
||||
base.Participate(lifecycle); |
|
||||
|
|
||||
lifecycle.Subscribe(GrainLifecycleStage.SetupState, OnSetupState); |
|
||||
} |
|
||||
|
|
||||
private async Task OnSetupState(CancellationToken ct) |
|
||||
{ |
|
||||
if (!ct.IsCancellationRequested) |
|
||||
{ |
|
||||
storage = runtime.GetStorage<TGrainState>(this); |
|
||||
|
|
||||
await OnSetupState(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private async Task OnSetupState() |
|
||||
{ |
|
||||
await ReadStateAsync(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,79 @@ |
|||||
|
// ==========================================================================
|
||||
|
// PubSubExtensions.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
#pragma warning disable 4014
|
||||
|
#pragma warning disable RECS0165 // Asynchronous methods should return a Task instead of void
|
||||
|
|
||||
|
namespace Squidex.Infrastructure |
||||
|
{ |
||||
|
public static class PubSubExtensions |
||||
|
{ |
||||
|
public class Request<T> |
||||
|
{ |
||||
|
public T Body { get; set; } |
||||
|
|
||||
|
public Guid CorrelationId { get; set; } |
||||
|
} |
||||
|
|
||||
|
public class Response<T> |
||||
|
{ |
||||
|
public T Body { get; set; } |
||||
|
|
||||
|
public Guid CorrelationId { get; set; } |
||||
|
} |
||||
|
|
||||
|
public static IDisposable ReceiveAsync<TRequest, TResponse>(this IPubSub pubsub, Func<TRequest, Task<TResponse>> callback, bool self = true) |
||||
|
{ |
||||
|
return pubsub.Subscribe<Request<TRequest>>(async x => |
||||
|
{ |
||||
|
var response = await callback(x.Body); |
||||
|
|
||||
|
pubsub.Publish(new Response<TResponse> { CorrelationId = x.CorrelationId, Body = response }, true); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public static async Task<TResponse> RequestAsync<TRequest, TResponse>(this IPubSub pubsub, TRequest message, TimeSpan timeout, bool self = true) |
||||
|
{ |
||||
|
var request = new Request<TRequest> { Body = message, CorrelationId = Guid.NewGuid() }; |
||||
|
|
||||
|
IDisposable subscription = null; |
||||
|
try |
||||
|
{ |
||||
|
var receiveTask = new TaskCompletionSource<TResponse>(); |
||||
|
|
||||
|
subscription = pubsub.Subscribe<Response<TResponse>>(response => |
||||
|
{ |
||||
|
if (response.CorrelationId == request.CorrelationId) |
||||
|
{ |
||||
|
receiveTask.SetResult(response.Body); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
Task.Run(() => pubsub.Publish(request, self)); |
||||
|
|
||||
|
var firstTask = await Task.WhenAny(receiveTask.Task, Task.Delay(timeout)); |
||||
|
|
||||
|
if (firstTask.Id != receiveTask.Task.Id) |
||||
|
{ |
||||
|
throw new TaskCanceledException(); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
return await receiveTask.Task; |
||||
|
} |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
subscription?.Dispose(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
// ==========================================================================
|
||||
|
// IStateFactory.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.States |
||||
|
{ |
||||
|
public interface IStateFactory |
||||
|
{ |
||||
|
Task<T> GetAsync<T, TState>(string key) where T : StatefulObject<TState>; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,21 @@ |
|||||
|
// ==========================================================================
|
||||
|
// IStateHolder.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.States |
||||
|
{ |
||||
|
public interface IStateHolder<T> |
||||
|
{ |
||||
|
T State { get; set; } |
||||
|
|
||||
|
Task ReadAsync(); |
||||
|
|
||||
|
Task WriteAsync(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
// ==========================================================================
|
||||
|
// IStateStore.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.States |
||||
|
{ |
||||
|
public interface IStateStore |
||||
|
{ |
||||
|
Task WriteAsync<T>(string key, T value, string oldEtag, string newEtag); |
||||
|
|
||||
|
Task<(T Value, string Etag)> ReadAsync<T>(string key); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,15 @@ |
|||||
|
// ==========================================================================
|
||||
|
// InvalidateMessage.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Infrastructure.States |
||||
|
{ |
||||
|
public sealed class InvalidateMessage |
||||
|
{ |
||||
|
public string Key { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,108 @@ |
|||||
|
// ==========================================================================
|
||||
|
// StateFactory.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.Extensions.Caching.Memory; |
||||
|
using Squidex.Infrastructure.Tasks; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.States |
||||
|
{ |
||||
|
public sealed class StateFactory : DisposableObjectBase, IExternalSystem, IStateFactory |
||||
|
{ |
||||
|
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10); |
||||
|
private readonly IPubSub pubSub; |
||||
|
private readonly IStateStore store; |
||||
|
private readonly IMemoryCache statesCache; |
||||
|
private readonly IServiceProvider services; |
||||
|
private readonly List<IDisposable> states = new List<IDisposable>(); |
||||
|
private readonly SingleThreadedDispatcher cleanupDispatcher = new SingleThreadedDispatcher(); |
||||
|
private IDisposable pubSubscription; |
||||
|
|
||||
|
public StateFactory( |
||||
|
IPubSub pubSub, |
||||
|
IServiceProvider services, |
||||
|
IStateStore store, |
||||
|
IMemoryCache statesCache) |
||||
|
{ |
||||
|
Guard.NotNull(pubSub, nameof(pubSub)); |
||||
|
Guard.NotNull(store, nameof(store)); |
||||
|
Guard.NotNull(services, nameof(services)); |
||||
|
Guard.NotNull(statesCache, nameof(statesCache)); |
||||
|
|
||||
|
this.pubSub = pubSub; |
||||
|
this.store = store; |
||||
|
this.services = services; |
||||
|
this.statesCache = statesCache; |
||||
|
} |
||||
|
|
||||
|
public void Connect() |
||||
|
{ |
||||
|
pubSubscription = pubSub.Subscribe<InvalidateMessage>(m => |
||||
|
{ |
||||
|
statesCache.Remove(m.Key); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public async Task<T> GetAsync<T, TState>(string key) where T : StatefulObject<TState> |
||||
|
{ |
||||
|
Guard.NotNull(key, nameof(key)); |
||||
|
|
||||
|
if (statesCache.TryGetValue<T>(key, out var state)) |
||||
|
{ |
||||
|
return state; |
||||
|
} |
||||
|
|
||||
|
state = (T)services.GetService(typeof(T)); |
||||
|
|
||||
|
var stateHolder = new StateHolder<TState>(key, () => |
||||
|
{ |
||||
|
pubSub.Publish(new InvalidateMessage { Key = key }, false); |
||||
|
}, store); |
||||
|
|
||||
|
await state.ActivateAsync(stateHolder); |
||||
|
|
||||
|
var stateEntry = statesCache.CreateEntry(key); |
||||
|
|
||||
|
stateEntry.Value = state; |
||||
|
stateEntry.AbsoluteExpirationRelativeToNow = CacheDuration; |
||||
|
|
||||
|
stateEntry.PostEvictionCallbacks.Add(new PostEvictionCallbackRegistration |
||||
|
{ |
||||
|
EvictionCallback = (k, v, r, s) => |
||||
|
{ |
||||
|
cleanupDispatcher.DispatchAsync(() => |
||||
|
{ |
||||
|
state.Dispose(); |
||||
|
states.Remove(state); |
||||
|
}).Forget(); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
states.Add(state); |
||||
|
|
||||
|
return state; |
||||
|
} |
||||
|
|
||||
|
protected override void DisposeObject(bool disposing) |
||||
|
{ |
||||
|
if (disposing) |
||||
|
{ |
||||
|
cleanupDispatcher.DispatchAsync(() => |
||||
|
{ |
||||
|
foreach (var state in states) |
||||
|
{ |
||||
|
state.Dispose(); |
||||
|
} |
||||
|
}); |
||||
|
cleanupDispatcher.StopAndWaitAsync().Wait(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,44 @@ |
|||||
|
// ==========================================================================
|
||||
|
// StateHolder.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.States |
||||
|
{ |
||||
|
public sealed class StateHolder<T> : IStateHolder<T> |
||||
|
{ |
||||
|
private readonly Action written; |
||||
|
private readonly IStateStore store; |
||||
|
private readonly string key; |
||||
|
private string etag; |
||||
|
|
||||
|
public T State { get; set; } |
||||
|
|
||||
|
internal StateHolder(string key, Action written, IStateStore store) |
||||
|
{ |
||||
|
this.key = key; |
||||
|
this.written = written; |
||||
|
this.store = store; |
||||
|
} |
||||
|
|
||||
|
public async Task ReadAsync() |
||||
|
{ |
||||
|
(State, etag) = await store.ReadAsync<T>(key); |
||||
|
} |
||||
|
|
||||
|
public async Task WriteAsync() |
||||
|
{ |
||||
|
var newEtag = Guid.NewGuid().ToString(); |
||||
|
|
||||
|
await store.WriteAsync(key, State, etag, newEtag); |
||||
|
|
||||
|
etag = newEtag; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,65 @@ |
|||||
|
// ==========================================================================
|
||||
|
// StatefulActor.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.States |
||||
|
{ |
||||
|
public abstract class StatefulObject<T> : DisposableObjectBase |
||||
|
{ |
||||
|
private IStateHolder<T> stateHolder; |
||||
|
|
||||
|
public T State |
||||
|
{ |
||||
|
get |
||||
|
{ |
||||
|
if (stateHolder != null) |
||||
|
{ |
||||
|
return stateHolder.State; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
return default(T); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected set |
||||
|
{ |
||||
|
if (stateHolder != null) |
||||
|
{ |
||||
|
stateHolder.State = value; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public Task ActivateAsync(IStateHolder<T> stateHolder) |
||||
|
{ |
||||
|
Guard.NotNull(stateHolder, nameof(stateHolder)); |
||||
|
|
||||
|
this.stateHolder = stateHolder; |
||||
|
|
||||
|
return stateHolder.ReadAsync(); |
||||
|
} |
||||
|
|
||||
|
public async Task ReadStateAsync() |
||||
|
{ |
||||
|
if (stateHolder != null) |
||||
|
{ |
||||
|
await stateHolder.ReadAsync(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async Task WriteStateAsync() |
||||
|
{ |
||||
|
if (stateHolder != null) |
||||
|
{ |
||||
|
await stateHolder.WriteAsync(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,358 @@ |
|||||
|
// ==========================================================================
|
||||
|
// EventConsumerActorTests.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using FakeItEasy; |
||||
|
using FluentAssertions; |
||||
|
using Squidex.Infrastructure.Log; |
||||
|
using Squidex.Infrastructure.States; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.CQRS.Events.Actors |
||||
|
{ |
||||
|
public class EventConsumerActorTests |
||||
|
{ |
||||
|
public sealed class MyEvent : IEvent |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public sealed class MyEventConsumerActor : EventConsumerActor |
||||
|
{ |
||||
|
public MyEventConsumerActor(EventDataFormatter formatter, IEventStore eventStore, ISemanticLog log) |
||||
|
: base(formatter, eventStore, log) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
protected override IEventSubscription CreateSubscription(IEventStore eventStore, string streamFilter, string position) |
||||
|
{ |
||||
|
return eventStore.CreateSubscription(this, streamFilter, position); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private readonly IEventConsumer eventConsumer = A.Fake<IEventConsumer>(); |
||||
|
private readonly IEventStore eventStore = A.Fake<IEventStore>(); |
||||
|
private readonly IEventSubscriber sutSubscriber; |
||||
|
private readonly IEventSubscription eventSubscription = A.Fake<IEventSubscription>(); |
||||
|
private readonly ISemanticLog log = A.Fake<ISemanticLog>(); |
||||
|
private readonly IStateHolder<EventConsumerState> stateHolder = A.Fake<IStateHolder<EventConsumerState>>(); |
||||
|
private readonly EventDataFormatter formatter = A.Fake<EventDataFormatter>(); |
||||
|
private readonly EventData eventData = new EventData(); |
||||
|
private readonly Envelope<IEvent> envelope = new Envelope<IEvent>(new MyEvent()); |
||||
|
private readonly EventConsumerActor sut; |
||||
|
private readonly string consumerName; |
||||
|
private readonly string initialPosition = Guid.NewGuid().ToString(); |
||||
|
private EventConsumerState state = new EventConsumerState(); |
||||
|
|
||||
|
public EventConsumerActorTests() |
||||
|
{ |
||||
|
state.Position = initialPosition; |
||||
|
|
||||
|
consumerName = eventConsumer.GetType().Name; |
||||
|
|
||||
|
A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber>.Ignored, A<string>.Ignored, A<string>.Ignored)) |
||||
|
.Returns(eventSubscription); |
||||
|
|
||||
|
A.CallTo(() => eventConsumer.Name). |
||||
|
Returns(consumerName); |
||||
|
|
||||
|
A.CallTo(() => stateHolder.State) |
||||
|
.ReturnsLazily(() => state); |
||||
|
|
||||
|
A.CallToSet(() => stateHolder.State) |
||||
|
.Invokes(new Action<EventConsumerState>(s => state = s)); |
||||
|
|
||||
|
A.CallTo(() => formatter.Parse(eventData, true)).Returns(envelope); |
||||
|
|
||||
|
sut = new MyEventConsumerActor(formatter, eventStore, log); |
||||
|
sutSubscriber = sut; |
||||
|
|
||||
|
sut.ActivateAsync(stateHolder).Wait(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_not_subscribe_to_event_store_when_stopped_in_db() |
||||
|
{ |
||||
|
state = state.Stopped(); |
||||
|
|
||||
|
sut.Activate(eventConsumer); |
||||
|
sut.Dispose(); |
||||
|
|
||||
|
state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = null }); |
||||
|
|
||||
|
A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber>.Ignored, A<string>.Ignored, A<string>.Ignored)) |
||||
|
.MustNotHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_subscribe_to_event_store_when_not_found_in_db() |
||||
|
{ |
||||
|
sut.Activate(eventConsumer); |
||||
|
sut.Dispose(); |
||||
|
|
||||
|
state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = false, Position = initialPosition, Error = null }); |
||||
|
|
||||
|
A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber>.Ignored, A<string>.Ignored, A<string>.Ignored)) |
||||
|
.MustHaveHappened(Repeated.Exactly.Once); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_subscribe_to_event_store_when_not_stopped_in_db() |
||||
|
{ |
||||
|
sut.Activate(eventConsumer); |
||||
|
sut.Dispose(); |
||||
|
|
||||
|
state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = false, Position = initialPosition, Error = null }); |
||||
|
|
||||
|
A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber>.Ignored, A<string>.Ignored, A<string>.Ignored)) |
||||
|
.MustHaveHappened(Repeated.Exactly.Once); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_stop_subscription_when_stopped() |
||||
|
{ |
||||
|
sut.Activate(eventConsumer); |
||||
|
sut.Stop(); |
||||
|
sut.Stop(); |
||||
|
|
||||
|
sut.Dispose(); |
||||
|
|
||||
|
state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = null }); |
||||
|
|
||||
|
A.CallTo(() => stateHolder.WriteAsync()) |
||||
|
.MustHaveHappened(Repeated.Exactly.Once); |
||||
|
|
||||
|
A.CallTo(() => eventSubscription.StopAsync()) |
||||
|
.MustHaveHappened(Repeated.Exactly.Once); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_reset_consumer_when_resetting() |
||||
|
{ |
||||
|
sut.Activate(eventConsumer); |
||||
|
sut.Stop(); |
||||
|
sut.Reset(); |
||||
|
sut.Dispose(); |
||||
|
|
||||
|
state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = false, Position = null, Error = null }); |
||||
|
|
||||
|
A.CallTo(() => stateHolder.WriteAsync()) |
||||
|
.MustHaveHappened(Repeated.Exactly.Twice); |
||||
|
|
||||
|
A.CallTo(() => eventConsumer.ClearAsync()) |
||||
|
.MustHaveHappened(Repeated.Exactly.Once); |
||||
|
|
||||
|
A.CallTo(() => eventSubscription.StopAsync()) |
||||
|
.MustHaveHappened(Repeated.Exactly.Once); |
||||
|
|
||||
|
A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber>.Ignored, A<string>.Ignored, state.Position)) |
||||
|
.MustHaveHappened(Repeated.Exactly.Once); |
||||
|
|
||||
|
A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber>.Ignored, A<string>.Ignored, null)) |
||||
|
.MustHaveHappened(Repeated.Exactly.Once); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_invoke_and_update_position_when_event_received() |
||||
|
{ |
||||
|
sut.Activate(eventConsumer); |
||||
|
|
||||
|
var @event = new StoredEvent(Guid.NewGuid().ToString(), 123, eventData); |
||||
|
|
||||
|
await OnEventAsync(eventSubscription, @event); |
||||
|
|
||||
|
sut.Dispose(); |
||||
|
|
||||
|
state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = false, Position = @event.EventPosition, Error = null }); |
||||
|
|
||||
|
A.CallTo(() => stateHolder.WriteAsync()) |
||||
|
.MustHaveHappened(Repeated.Exactly.Once); |
||||
|
|
||||
|
A.CallTo(() => eventConsumer.On(envelope)) |
||||
|
.MustHaveHappened(Repeated.Exactly.Once); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_ignore_old_events() |
||||
|
{ |
||||
|
sut.Activate(eventConsumer); |
||||
|
|
||||
|
A.CallTo(() => formatter.Parse(eventData, true)) |
||||
|
.Throws(new TypeNameNotFoundException()); |
||||
|
|
||||
|
var @event = new StoredEvent(Guid.NewGuid().ToString(), 123, eventData); |
||||
|
|
||||
|
await OnEventAsync(eventSubscription, @event); |
||||
|
|
||||
|
sut.Dispose(); |
||||
|
|
||||
|
state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = false, Position = @event.EventPosition, Error = null }); |
||||
|
|
||||
|
A.CallTo(() => stateHolder.WriteAsync()) |
||||
|
.MustHaveHappened(Repeated.Exactly.Once); |
||||
|
|
||||
|
A.CallTo(() => eventConsumer.On(envelope)) |
||||
|
.MustNotHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_not_invoke_and_update_position_when_event_is_from_another_subscription() |
||||
|
{ |
||||
|
sut.Activate(eventConsumer); |
||||
|
|
||||
|
var @event = new StoredEvent(Guid.NewGuid().ToString(), 123, eventData); |
||||
|
|
||||
|
await OnEventAsync(A.Fake<IEventSubscription>(), @event); |
||||
|
|
||||
|
sut.Dispose(); |
||||
|
|
||||
|
state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = false, Position = initialPosition, Error = null }); |
||||
|
|
||||
|
A.CallTo(() => eventConsumer.On(envelope)) |
||||
|
.MustNotHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_not_make_error_handling_when_exception_is_from_another_subscription() |
||||
|
{ |
||||
|
sut.Activate(eventConsumer); |
||||
|
|
||||
|
var ex = new InvalidOperationException(); |
||||
|
|
||||
|
await OnErrorAsync(A.Fake<IEventSubscription>(), ex); |
||||
|
|
||||
|
sut.Dispose(); |
||||
|
|
||||
|
state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = false, Position = initialPosition, Error = null }); |
||||
|
|
||||
|
A.CallTo(() => stateHolder.WriteAsync()) |
||||
|
.MustNotHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_stop_if_resetting_failed() |
||||
|
{ |
||||
|
sut.Activate(eventConsumer); |
||||
|
|
||||
|
var ex = new InvalidOperationException(); |
||||
|
|
||||
|
A.CallTo(() => eventConsumer.ClearAsync()) |
||||
|
.Throws(ex); |
||||
|
|
||||
|
sut.Reset(); |
||||
|
sut.Dispose(); |
||||
|
|
||||
|
state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = ex.ToString() }); |
||||
|
|
||||
|
A.CallTo(() => stateHolder.WriteAsync()) |
||||
|
.MustHaveHappened(Repeated.Exactly.Once); |
||||
|
|
||||
|
A.CallTo(() => eventSubscription.StopAsync()) |
||||
|
.MustHaveHappened(Repeated.Exactly.Once); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_stop_if_handling_failed() |
||||
|
{ |
||||
|
sut.Activate(eventConsumer); |
||||
|
|
||||
|
var ex = new InvalidOperationException(); |
||||
|
|
||||
|
A.CallTo(() => eventConsumer.On(envelope)) |
||||
|
.Throws(ex); |
||||
|
|
||||
|
var @event = new StoredEvent(Guid.NewGuid().ToString(), 123, eventData); |
||||
|
|
||||
|
await OnEventAsync(eventSubscription, @event); |
||||
|
|
||||
|
sut.Dispose(); |
||||
|
|
||||
|
state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = ex.ToString() }); |
||||
|
|
||||
|
A.CallTo(() => eventConsumer.On(envelope)) |
||||
|
.MustHaveHappened(); |
||||
|
|
||||
|
A.CallTo(() => stateHolder.WriteAsync()) |
||||
|
.MustHaveHappened(Repeated.Exactly.Once); |
||||
|
|
||||
|
A.CallTo(() => eventSubscription.StopAsync()) |
||||
|
.MustHaveHappened(Repeated.Exactly.Once); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_stop_if_deserialization_failed() |
||||
|
{ |
||||
|
sut.Activate(eventConsumer); |
||||
|
|
||||
|
var ex = new InvalidOperationException(); |
||||
|
|
||||
|
A.CallTo(() => formatter.Parse(eventData, true)) |
||||
|
.Throws(ex); |
||||
|
|
||||
|
var @event = new StoredEvent(Guid.NewGuid().ToString(), 123, eventData); |
||||
|
|
||||
|
await OnEventAsync(eventSubscription, @event); |
||||
|
|
||||
|
sut.Dispose(); |
||||
|
|
||||
|
state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = ex.ToString() }); |
||||
|
|
||||
|
A.CallTo(() => eventConsumer.On(envelope)) |
||||
|
.MustNotHaveHappened(); |
||||
|
|
||||
|
A.CallTo(() => stateHolder.WriteAsync()) |
||||
|
.MustHaveHappened(Repeated.Exactly.Once); |
||||
|
|
||||
|
A.CallTo(() => eventSubscription.StopAsync()) |
||||
|
.MustHaveHappened(Repeated.Exactly.Once); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_start_after_stop_when_handling_failed() |
||||
|
{ |
||||
|
sut.Activate(eventConsumer); |
||||
|
|
||||
|
var exception = new InvalidOperationException(); |
||||
|
|
||||
|
A.CallTo(() => eventConsumer.On(envelope)) |
||||
|
.Throws(exception); |
||||
|
|
||||
|
var @event = new StoredEvent(Guid.NewGuid().ToString(), 123, eventData); |
||||
|
|
||||
|
await OnEventAsync(eventSubscription, @event); |
||||
|
|
||||
|
sut.Start(); |
||||
|
sut.Start(); |
||||
|
sut.Dispose(); |
||||
|
|
||||
|
state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = false, Position = initialPosition, Error = null }); |
||||
|
|
||||
|
A.CallTo(() => eventConsumer.On(envelope)) |
||||
|
.MustHaveHappened(); |
||||
|
|
||||
|
A.CallTo(() => stateHolder.WriteAsync()) |
||||
|
.MustHaveHappened(Repeated.Exactly.Twice); |
||||
|
|
||||
|
A.CallTo(() => eventSubscription.StopAsync()) |
||||
|
.MustHaveHappened(Repeated.Exactly.Once); |
||||
|
|
||||
|
A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber>.Ignored, A<string>.Ignored, A<string>.Ignored)) |
||||
|
.MustHaveHappened(Repeated.Exactly.Twice); |
||||
|
} |
||||
|
|
||||
|
private Task OnErrorAsync(IEventSubscription subscriber, Exception ex) |
||||
|
{ |
||||
|
return sutSubscriber.OnErrorAsync(subscriber, ex); |
||||
|
} |
||||
|
|
||||
|
private Task OnEventAsync(IEventSubscription subscriber, StoredEvent ev) |
||||
|
{ |
||||
|
return sutSubscriber.OnEventAsync(subscriber, ev); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,125 @@ |
|||||
|
// ==========================================================================
|
||||
|
// EventConsumerManagerTests.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using FakeItEasy; |
||||
|
using FluentAssertions; |
||||
|
using Squidex.Infrastructure.CQRS.Events.Actors.Messages; |
||||
|
using Squidex.Infrastructure.States; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.CQRS.Events.Actors |
||||
|
{ |
||||
|
public class EventConsumerManagerTests |
||||
|
{ |
||||
|
private readonly EventConsumerActor actor1 = A.Fake<EventConsumerActor>(); |
||||
|
private readonly EventConsumerActor actor2 = A.Fake<EventConsumerActor>(); |
||||
|
private readonly IStateFactory factory = A.Fake<IStateFactory>(); |
||||
|
private readonly IEventConsumer consumer1 = A.Fake<IEventConsumer>(); |
||||
|
private readonly IEventConsumer consumer2 = A.Fake<IEventConsumer>(); |
||||
|
private readonly IPubSub pubSub = new InMemoryPubSub(); |
||||
|
private readonly string consumerName1 = "Consumer1"; |
||||
|
private readonly string consumerName2 = "Consumer2"; |
||||
|
private readonly EventConsumerActorManager sut; |
||||
|
|
||||
|
public EventConsumerManagerTests() |
||||
|
{ |
||||
|
A.CallTo(() => consumer1.Name).Returns(consumerName1); |
||||
|
A.CallTo(() => consumer2.Name).Returns(consumerName2); |
||||
|
|
||||
|
A.CallTo(() => factory.GetAsync<EventConsumerActor, EventConsumerState>(consumerName1)).Returns(actor1); |
||||
|
A.CallTo(() => factory.GetAsync<EventConsumerActor, EventConsumerState>(consumerName2)).Returns(actor2); |
||||
|
|
||||
|
sut = new EventConsumerActorManager(new IEventConsumer[] { consumer1, consumer2 }, pubSub, factory); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_activate_all_actors() |
||||
|
{ |
||||
|
sut.Connect(); |
||||
|
|
||||
|
A.CallTo(() => actor1.Activate(consumer1)) |
||||
|
.MustHaveHappened(); |
||||
|
|
||||
|
A.CallTo(() => actor2.Activate(consumer2)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_start_correct_actor() |
||||
|
{ |
||||
|
sut.Connect(); |
||||
|
|
||||
|
pubSub.Publish(new StartConsumerMessage { ConsumerName = consumerName1 }, true); |
||||
|
|
||||
|
A.CallTo(() => actor1.Start()) |
||||
|
.MustHaveHappened(); |
||||
|
|
||||
|
A.CallTo(() => actor2.Start()) |
||||
|
.MustNotHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_stop_correct_actor() |
||||
|
{ |
||||
|
sut.Connect(); |
||||
|
|
||||
|
pubSub.Publish(new StopConsumerMessage { ConsumerName = consumerName1 }, true); |
||||
|
|
||||
|
A.CallTo(() => actor1.Stop()) |
||||
|
.MustHaveHappened(); |
||||
|
|
||||
|
A.CallTo(() => actor2.Stop()) |
||||
|
.MustNotHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_reset_correct_actor() |
||||
|
{ |
||||
|
sut.Connect(); |
||||
|
|
||||
|
pubSub.Publish(new ResetConsumerMessage { ConsumerName = consumerName2 }, true); |
||||
|
|
||||
|
A.CallTo(() => actor1.Reset()) |
||||
|
.MustNotHaveHappened(); |
||||
|
|
||||
|
A.CallTo(() => actor2.Reset()) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_get_state_from_all_actors() |
||||
|
{ |
||||
|
sut.Connect(); |
||||
|
|
||||
|
A.CallTo(() => actor1.GetState()) |
||||
|
.Returns(new EventConsumerInfo { Name = consumerName1, Position = "123 " }); |
||||
|
|
||||
|
A.CallTo(() => actor2.GetState()) |
||||
|
.Returns(new EventConsumerInfo { Name = consumerName2, Position = "345 " }); |
||||
|
|
||||
|
var response = await pubSub.RequestAsync<GetStatesRequest, GetStatesResponse>(new GetStatesRequest(), TimeSpan.FromSeconds(5), true); |
||||
|
|
||||
|
response.States.ShouldAllBeEquivalentTo(new EventConsumerInfo[] |
||||
|
{ |
||||
|
new EventConsumerInfo { Name = consumerName1, Position = "123 " }, |
||||
|
new EventConsumerInfo { Name = consumerName2, Position = "345 " } |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_not_dispose_actors() |
||||
|
{ |
||||
|
sut.Dispose(); |
||||
|
|
||||
|
Assert.False(actor1.IsDisposed); |
||||
|
Assert.False(actor2.IsDisposed); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,58 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// EventConsumerBootstrapTests.cs
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex Group
|
|
||||
// All rights reserved.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System.Threading.Tasks; |
|
||||
using FakeItEasy; |
|
||||
using Orleans; |
|
||||
using Orleans.Providers; |
|
||||
using Squidex.Infrastructure.CQRS.Events.Orleans.Grains; |
|
||||
using Xunit; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.CQRS.Events.Grains |
|
||||
{ |
|
||||
public sealed class EventConsumerBootstrapTests |
|
||||
{ |
|
||||
private readonly IEventConsumerRegistryGrain registry = A.Fake<IEventConsumerRegistryGrain>(); |
|
||||
private readonly IProviderRuntime runtime = A.Fake<IProviderRuntime>(); |
|
||||
private readonly EventConsumerBootstrap sut = new EventConsumerBootstrap(); |
|
||||
|
|
||||
public EventConsumerBootstrapTests() |
|
||||
{ |
|
||||
var factory = A.Fake<IGrainFactory>(); |
|
||||
|
|
||||
A.CallTo(() => factory.GetGrain<IEventConsumerRegistryGrain>("Default", null)) |
|
||||
.Returns(registry); |
|
||||
|
|
||||
A.CallTo(() => runtime.GrainFactory) |
|
||||
.Returns(factory); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_do_nothing_on_close() |
|
||||
{ |
|
||||
await sut.Close(); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_set_name_on_init() |
|
||||
{ |
|
||||
await sut.Init("MyName", runtime, null); |
|
||||
|
|
||||
Assert.Equal("MyName", sut.Name); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_activate_registry_on_init() |
|
||||
{ |
|
||||
await sut.Init("MyName", runtime, null); |
|
||||
|
|
||||
A.CallTo(() => registry.ActivateAsync(null)) |
|
||||
.MustHaveHappened(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,408 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// EventConsumerGrainTests.cs
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex Group
|
|
||||
// All rights reserved.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.Threading.Tasks; |
|
||||
using FakeItEasy; |
|
||||
using Orleans.Concurrency; |
|
||||
using Orleans.Core; |
|
||||
using Orleans.Runtime; |
|
||||
using Squidex.Infrastructure.CQRS.Events.Orleans.Grains; |
|
||||
using Squidex.Infrastructure.CQRS.Events.Orleans.Grains.Implementation; |
|
||||
using Squidex.Infrastructure.Log; |
|
||||
using Xunit; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.CQRS.Events.Grains |
|
||||
{ |
|
||||
public class EventConsumerGrainTests |
|
||||
{ |
|
||||
public sealed class MyEvent : IEvent |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public sealed class MyEventConsumerGrain : EventConsumerGrain |
|
||||
{ |
|
||||
public MyEventConsumerGrain( |
|
||||
EventDataFormatter formatter, |
|
||||
EventConsumerFactory eventConsumerFactory, |
|
||||
IEventStore eventStore, |
|
||||
ISemanticLog log, |
|
||||
IGrainIdentity identity, |
|
||||
IGrainRuntime runtime, |
|
||||
IStorage<EventConsumerGrainState> storage) |
|
||||
: base(formatter, eventConsumerFactory, eventStore, log, identity, runtime, storage) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
protected override IEventConsumerGrain GetSelf() |
|
||||
{ |
|
||||
return this; |
|
||||
} |
|
||||
|
|
||||
protected override IEventSubscription CreateSubscription(IEventSubscriber subscriber, string streamFilter, string position) |
|
||||
{ |
|
||||
return EventStore.CreateSubscription(subscriber, streamFilter, position); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private readonly IEventConsumer eventConsumer = A.Fake<IEventConsumer>(); |
|
||||
private readonly IEventStore eventStore = A.Fake<IEventStore>(); |
|
||||
private readonly IEventSubscription eventSubscription = A.Fake<IEventSubscription>(); |
|
||||
private readonly ISemanticLog log = A.Fake<ISemanticLog>(); |
|
||||
private readonly IStorage<EventConsumerGrainState> storage = A.Fake<IStorage<EventConsumerGrainState>>(); |
|
||||
private readonly EventDataFormatter formatter = A.Fake<EventDataFormatter>(); |
|
||||
private readonly EventData eventData = new EventData(); |
|
||||
private readonly Envelope<IEvent> envelope = new Envelope<IEvent>(new MyEvent()); |
|
||||
private readonly EventConsumerFactory factory; |
|
||||
private readonly MyEventConsumerGrain sut; |
|
||||
private readonly string consumerName; |
|
||||
private EventConsumerGrainState state = new EventConsumerGrainState(); |
|
||||
|
|
||||
public EventConsumerGrainTests() |
|
||||
{ |
|
||||
factory = x => eventConsumer; |
|
||||
|
|
||||
state.Position = Guid.NewGuid().ToString(); |
|
||||
consumerName = eventConsumer.GetType().Name; |
|
||||
|
|
||||
A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber>.Ignored, A<string>.Ignored, A<string>.Ignored)).Returns(eventSubscription); |
|
||||
A.CallTo(() => eventConsumer.Name).Returns(consumerName); |
|
||||
|
|
||||
A.CallTo(() => formatter.Parse(eventData, true)).Returns(envelope); |
|
||||
|
|
||||
A.CallTo(() => storage.State).ReturnsLazily(() => state); |
|
||||
A.CallToSet(() => storage.State).Invokes(new Action<EventConsumerGrainState>(s => state = s)); |
|
||||
|
|
||||
sut = new MyEventConsumerGrain( |
|
||||
formatter, |
|
||||
factory, |
|
||||
eventStore, |
|
||||
log, |
|
||||
A.Fake<IGrainIdentity>(), |
|
||||
A.Fake<IGrainRuntime>(), |
|
||||
storage); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_not_subscribe_to_event_store_when_stopped_in_db() |
|
||||
{ |
|
||||
state.IsStopped = true; |
|
||||
|
|
||||
await sut.OnActivateAsync(); |
|
||||
await sut.ActivateAsync(); |
|
||||
|
|
||||
A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber>.Ignored, A<string>.Ignored, A<string>.Ignored)) |
|
||||
.MustNotHaveHappened(); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_subscribe_to_event_store_when_not_stopped_in_db() |
|
||||
{ |
|
||||
state.Position = "123"; |
|
||||
|
|
||||
await sut.OnActivateAsync(); |
|
||||
await sut.ActivateAsync(); |
|
||||
|
|
||||
A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber>.Ignored, A<string>.Ignored, "123")) |
|
||||
.MustHaveHappened(Repeated.Exactly.Once); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_stop_subscription_when_stopped() |
|
||||
{ |
|
||||
await sut.OnActivateAsync(); |
|
||||
await sut.ActivateAsync(); |
|
||||
await sut.StopAsync(); |
|
||||
await sut.StopAsync(); |
|
||||
|
|
||||
A.CallTo(() => eventSubscription.StopAsync()) |
|
||||
.MustHaveHappened(Repeated.Exactly.Once); |
|
||||
|
|
||||
Assert.True(state.IsStopped); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_reset_consumer_when_resetting() |
|
||||
{ |
|
||||
await sut.OnActivateAsync(); |
|
||||
await sut.ActivateAsync(); |
|
||||
await sut.StopAsync(); |
|
||||
await sut.ResetAsync(); |
|
||||
|
|
||||
A.CallTo(() => eventConsumer.ClearAsync()) |
|
||||
.MustHaveHappened(Repeated.Exactly.Once); |
|
||||
|
|
||||
A.CallTo(() => eventSubscription.StopAsync()) |
|
||||
.MustHaveHappened(Repeated.Exactly.Once); |
|
||||
|
|
||||
A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber>.Ignored, A<string>.Ignored, state.Position)) |
|
||||
.MustHaveHappened(Repeated.Exactly.Once); |
|
||||
|
|
||||
A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber>.Ignored, A<string>.Ignored, null)) |
|
||||
.MustHaveHappened(Repeated.Exactly.Once); |
|
||||
|
|
||||
Assert.False(state.IsStopped); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_unsubscribe_from_subscription_when_closed() |
|
||||
{ |
|
||||
await sut.OnActivateAsync(); |
|
||||
await sut.ActivateAsync(); |
|
||||
|
|
||||
await OnClosedAsync(eventSubscription); |
|
||||
|
|
||||
A.CallTo(() => eventSubscription.StopAsync()) |
|
||||
.MustHaveHappened(); |
|
||||
|
|
||||
Assert.False(state.IsStopped); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_not_unsubscribe_from_subscription_when_closed_call_is_from_another_subscription() |
|
||||
{ |
|
||||
await sut.OnActivateAsync(); |
|
||||
await sut.ActivateAsync(); |
|
||||
|
|
||||
await OnClosedAsync(A.Fake<IEventSubscription>()); |
|
||||
|
|
||||
A.CallTo(() => eventSubscription.StopAsync()) |
|
||||
.MustNotHaveHappened(); |
|
||||
|
|
||||
Assert.False(state.IsStopped); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_not_unsubscribe_from_subscription_when_not_running() |
|
||||
{ |
|
||||
state.IsStopped = true; |
|
||||
|
|
||||
await sut.OnActivateAsync(); |
|
||||
await sut.ActivateAsync(); |
|
||||
|
|
||||
await OnClosedAsync(A.Fake<IEventSubscription>()); |
|
||||
|
|
||||
A.CallTo(() => storage.WriteStateAsync()) |
|
||||
.MustNotHaveHappened(); |
|
||||
|
|
||||
Assert.True(state.IsStopped); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_invoke_and_update_position_when_event_received() |
|
||||
{ |
|
||||
var @event = new StoredEvent(Guid.NewGuid().ToString(), 123, eventData); |
|
||||
|
|
||||
await sut.OnActivateAsync(); |
|
||||
await sut.ActivateAsync(); |
|
||||
|
|
||||
await OnEventAsync(eventSubscription, @event); |
|
||||
|
|
||||
A.CallTo(() => eventConsumer.On(envelope)) |
|
||||
.MustHaveHappened(Repeated.Exactly.Once); |
|
||||
|
|
||||
Assert.Equal(@event.EventPosition, state.Position); |
|
||||
|
|
||||
var info = await sut.GetStateAsync(); |
|
||||
|
|
||||
Assert.Equal(@event.EventPosition, info.Value.Position); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_ignore_old_events() |
|
||||
{ |
|
||||
A.CallTo(() => formatter.Parse(eventData, true)) |
|
||||
.Throws(new TypeNameNotFoundException()); |
|
||||
|
|
||||
var @event = new StoredEvent(Guid.NewGuid().ToString(), 123, eventData); |
|
||||
|
|
||||
await sut.OnActivateAsync(); |
|
||||
await sut.ActivateAsync(); |
|
||||
|
|
||||
await OnEventAsync(eventSubscription, @event); |
|
||||
|
|
||||
A.CallTo(() => eventConsumer.On(envelope)) |
|
||||
.MustNotHaveHappened(); |
|
||||
|
|
||||
Assert.Equal(@event.EventPosition, state.Position); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_not_invoke_and_update_position_when_event_is_from_another_subscription() |
|
||||
{ |
|
||||
var @event = new StoredEvent(Guid.NewGuid().ToString(), 123, eventData); |
|
||||
|
|
||||
await sut.OnActivateAsync(); |
|
||||
await sut.ActivateAsync(); |
|
||||
|
|
||||
await OnEventAsync(A.Fake<IEventSubscription>(), @event); |
|
||||
|
|
||||
A.CallTo(() => eventConsumer.On(envelope)) |
|
||||
.MustNotHaveHappened(); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_not_make_error_handling_when_exception_is_from_another_subscription() |
|
||||
{ |
|
||||
var ex = new InvalidOperationException(); |
|
||||
|
|
||||
await sut.OnActivateAsync(); |
|
||||
await sut.ActivateAsync(); |
|
||||
|
|
||||
await OnErrorAsync(A.Fake<IEventSubscription>(), ex); |
|
||||
|
|
||||
Assert.False(state.IsStopped); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_stop_if_subscription_failed() |
|
||||
{ |
|
||||
var ex = new InvalidOperationException(); |
|
||||
|
|
||||
await sut.OnActivateAsync(); |
|
||||
await sut.ActivateAsync(); |
|
||||
|
|
||||
await OnErrorAsync(eventSubscription, ex); |
|
||||
|
|
||||
A.CallTo(() => eventSubscription.StopAsync()) |
|
||||
.MustHaveHappened(Repeated.Exactly.Once); |
|
||||
|
|
||||
Assert.True(state.IsStopped); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_stop_if_subscription_failed_and_ignore_error_on_unsubscribe() |
|
||||
{ |
|
||||
A.CallTo(() => eventSubscription.StopAsync()) |
|
||||
.Throws(new InvalidOperationException()); |
|
||||
|
|
||||
var ex = new InvalidOperationException(); |
|
||||
|
|
||||
await sut.OnActivateAsync(); |
|
||||
await sut.ActivateAsync(); |
|
||||
|
|
||||
await OnErrorAsync(eventSubscription, ex); |
|
||||
|
|
||||
Assert.True(state.IsStopped); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_stop_if_resetting_failed() |
|
||||
{ |
|
||||
var ex = new InvalidOperationException(); |
|
||||
|
|
||||
A.CallTo(() => eventConsumer.ClearAsync()) |
|
||||
.Throws(ex); |
|
||||
|
|
||||
await sut.OnActivateAsync(); |
|
||||
await sut.ActivateAsync(); |
|
||||
|
|
||||
await sut.ResetAsync(); |
|
||||
|
|
||||
A.CallTo(() => eventSubscription.StopAsync()) |
|
||||
.MustHaveHappened(Repeated.Exactly.Once); |
|
||||
|
|
||||
Assert.True(state.IsStopped); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_stop_if_handling_failed() |
|
||||
{ |
|
||||
var ex = new InvalidOperationException(); |
|
||||
|
|
||||
A.CallTo(() => eventConsumer.On(envelope)) |
|
||||
.Throws(ex); |
|
||||
|
|
||||
var @event = new StoredEvent(Guid.NewGuid().ToString(), 123, eventData); |
|
||||
|
|
||||
await sut.OnActivateAsync(); |
|
||||
await sut.ActivateAsync(); |
|
||||
|
|
||||
await OnEventAsync(eventSubscription, @event); |
|
||||
|
|
||||
A.CallTo(() => eventConsumer.On(envelope)) |
|
||||
.MustHaveHappened(); |
|
||||
|
|
||||
A.CallTo(() => eventSubscription.StopAsync()) |
|
||||
.MustHaveHappened(Repeated.Exactly.Once); |
|
||||
|
|
||||
Assert.True(state.IsStopped); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_stop_if_deserialization_failed() |
|
||||
{ |
|
||||
var ex = new InvalidOperationException(); |
|
||||
|
|
||||
A.CallTo(() => formatter.Parse(eventData, true)) |
|
||||
.Throws(ex); |
|
||||
|
|
||||
var @event = new StoredEvent(Guid.NewGuid().ToString(), 123, eventData); |
|
||||
|
|
||||
await sut.OnActivateAsync(); |
|
||||
await sut.ActivateAsync(); |
|
||||
|
|
||||
await OnEventAsync(eventSubscription, @event); |
|
||||
|
|
||||
A.CallTo(() => eventConsumer.On(envelope)) |
|
||||
.MustNotHaveHappened(); |
|
||||
|
|
||||
A.CallTo(() => eventSubscription.StopAsync()) |
|
||||
.MustHaveHappened(Repeated.Exactly.Once); |
|
||||
|
|
||||
Assert.True(state.IsStopped); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_start_after_stop_when_handling_failed() |
|
||||
{ |
|
||||
var exception = new InvalidOperationException(); |
|
||||
|
|
||||
A.CallTo(() => eventConsumer.On(envelope)) |
|
||||
.Throws(exception); |
|
||||
|
|
||||
var @event = new StoredEvent(Guid.NewGuid().ToString(), 123, eventData); |
|
||||
|
|
||||
await sut.OnActivateAsync(); |
|
||||
await sut.ActivateAsync(); |
|
||||
|
|
||||
await OnEventAsync(eventSubscription, @event); |
|
||||
|
|
||||
Assert.True(state.IsStopped); |
|
||||
|
|
||||
await sut.StartAsync(); |
|
||||
await sut.StartAsync(); |
|
||||
|
|
||||
A.CallTo(() => eventConsumer.On(envelope)) |
|
||||
.MustHaveHappened(); |
|
||||
|
|
||||
A.CallTo(() => eventSubscription.StopAsync()) |
|
||||
.MustHaveHappened(Repeated.Exactly.Once); |
|
||||
|
|
||||
A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber>.Ignored, A<string>.Ignored, A<string>.Ignored)) |
|
||||
.MustHaveHappened(Repeated.Exactly.Twice); |
|
||||
|
|
||||
Assert.False(state.IsStopped); |
|
||||
} |
|
||||
|
|
||||
private Task OnErrorAsync(IEventSubscription subscriber, Exception ex) |
|
||||
{ |
|
||||
return sut.OnErrorAsync(subscriber.AsImmutable(), ex.AsImmutable()); |
|
||||
} |
|
||||
|
|
||||
private Task OnEventAsync(IEventSubscription subscriber, StoredEvent ev) |
|
||||
{ |
|
||||
return sut.OnEventAsync(subscriber.AsImmutable(), ev.AsImmutable()); |
|
||||
} |
|
||||
|
|
||||
private Task OnClosedAsync(IEventSubscription subscriber) |
|
||||
{ |
|
||||
return sut.OnClosedAsync(subscriber.AsImmutable()); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,165 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// EventConsumerRegistryGrainTests.cs
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex Group
|
|
||||
// All rights reserved.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System.Collections.Generic; |
|
||||
using System.Threading.Tasks; |
|
||||
using FakeItEasy; |
|
||||
using FluentAssertions; |
|
||||
using Orleans; |
|
||||
using Orleans.Concurrency; |
|
||||
using Orleans.Core; |
|
||||
using Orleans.Runtime; |
|
||||
using Squidex.Infrastructure.CQRS.Events.Orleans.Grains; |
|
||||
using Squidex.Infrastructure.CQRS.Events.Orleans.Grains.Implementation; |
|
||||
using Xunit; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.CQRS.Events.Grains |
|
||||
{ |
|
||||
public class EventConsumerRegistryGrainTests |
|
||||
{ |
|
||||
public class MyEventConsumerRegistryGrain : EventConsumerRegistryGrain |
|
||||
{ |
|
||||
public MyEventConsumerRegistryGrain( |
|
||||
IEnumerable<IEventConsumer> eventConsumers, |
|
||||
IGrainIdentity identity, |
|
||||
IGrainRuntime runtime) |
|
||||
: base(eventConsumers, identity, runtime) |
|
||||
{ |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private readonly IEventConsumer consumerA = A.Fake<IEventConsumer>(); |
|
||||
private readonly IEventConsumer consumerB = A.Fake<IEventConsumer>(); |
|
||||
private readonly IEventConsumerGrain grainA = A.Fake<IEventConsumerGrain>(); |
|
||||
private readonly IEventConsumerGrain grainB = A.Fake<IEventConsumerGrain>(); |
|
||||
private readonly MyEventConsumerRegistryGrain sut; |
|
||||
|
|
||||
public EventConsumerRegistryGrainTests() |
|
||||
{ |
|
||||
var grainRuntime = A.Fake<IGrainRuntime>(); |
|
||||
var grainFactory = A.Fake<IGrainFactory>(); |
|
||||
|
|
||||
A.CallTo(() => grainFactory.GetGrain<IEventConsumerGrain>("a", null)).Returns(grainA); |
|
||||
A.CallTo(() => grainFactory.GetGrain<IEventConsumerGrain>("b", null)).Returns(grainB); |
|
||||
A.CallTo(() => grainRuntime.GrainFactory).Returns(grainFactory); |
|
||||
|
|
||||
A.CallTo(() => consumerA.Name).Returns("a"); |
|
||||
A.CallTo(() => consumerA.EventsFilter).Returns("^a-"); |
|
||||
|
|
||||
A.CallTo(() => consumerB.Name).Returns("b"); |
|
||||
A.CallTo(() => consumerB.EventsFilter).Returns("^b-"); |
|
||||
|
|
||||
sut = new MyEventConsumerRegistryGrain(new[] { consumerA, consumerB }, A.Fake<IGrainIdentity>(), grainRuntime); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_not_activate_all_grains_on_activate() |
|
||||
{ |
|
||||
await sut.OnActivateAsync(); |
|
||||
|
|
||||
A.CallTo(() => grainA.ActivateAsync()) |
|
||||
.MustNotHaveHappened(); |
|
||||
|
|
||||
A.CallTo(() => grainB.ActivateAsync()) |
|
||||
.MustNotHaveHappened(); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_activate_all_grains_on_reminder() |
|
||||
{ |
|
||||
await sut.ReceiveReminder(null, default(TickStatus)); |
|
||||
|
|
||||
A.CallTo(() => grainA.ActivateAsync()) |
|
||||
.MustHaveHappened(); |
|
||||
|
|
||||
A.CallTo(() => grainB.ActivateAsync()) |
|
||||
.MustHaveHappened(); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_activate_all_grains_on_activate_with_null() |
|
||||
{ |
|
||||
await sut.ActivateAsync(null); |
|
||||
|
|
||||
A.CallTo(() => grainA.ActivateAsync()) |
|
||||
.MustHaveHappened(); |
|
||||
|
|
||||
A.CallTo(() => grainB.ActivateAsync()) |
|
||||
.MustHaveHappened(); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_activate_matching_grains_when_stream_name_defined() |
|
||||
{ |
|
||||
await sut.ActivateAsync("a-123"); |
|
||||
|
|
||||
A.CallTo(() => grainA.ActivateAsync()) |
|
||||
.MustHaveHappened(); |
|
||||
|
|
||||
A.CallTo(() => grainB.ActivateAsync()) |
|
||||
.MustNotHaveHappened(); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_start_matching_grain() |
|
||||
{ |
|
||||
await sut.StartAsync("a"); |
|
||||
|
|
||||
A.CallTo(() => grainA.StartAsync()) |
|
||||
.MustHaveHappened(); |
|
||||
|
|
||||
A.CallTo(() => grainB.StartAsync()) |
|
||||
.MustNotHaveHappened(); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_stop_matching_grain() |
|
||||
{ |
|
||||
await sut.StopAsync("b"); |
|
||||
|
|
||||
A.CallTo(() => grainA.StopAsync()) |
|
||||
.MustNotHaveHappened(); |
|
||||
|
|
||||
A.CallTo(() => grainB.StopAsync()) |
|
||||
.MustHaveHappened(); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_reset_matching_grain() |
|
||||
{ |
|
||||
await sut.ResetAsync("b"); |
|
||||
|
|
||||
A.CallTo(() => grainA.ResetAsync()) |
|
||||
.MustNotHaveHappened(); |
|
||||
|
|
||||
A.CallTo(() => grainB.ResetAsync()) |
|
||||
.MustHaveHappened(); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_fetch_infos_from_all_grains() |
|
||||
{ |
|
||||
A.CallTo(() => grainA.GetStateAsync()) |
|
||||
.Returns(new Immutable<EventConsumerInfo>( |
|
||||
new EventConsumerInfo { Name = "A", Error = "A-Error", IsStopped = false, Position = "123" })); |
|
||||
|
|
||||
A.CallTo(() => grainB.GetStateAsync()) |
|
||||
.Returns(new Immutable<EventConsumerInfo>( |
|
||||
new EventConsumerInfo { Name = "B", Error = "B-Error", IsStopped = false, Position = "456" })); |
|
||||
|
|
||||
var infos = await sut.GetConsumersAsync(); |
|
||||
|
|
||||
infos.Value.ShouldBeEquivalentTo( |
|
||||
new List<EventConsumerInfo> |
|
||||
{ |
|
||||
new EventConsumerInfo { Name = "A", Error = "A-Error", IsStopped = false, Position = "123" }, |
|
||||
new EventConsumerInfo { Name = "B", Error = "B-Error", IsStopped = false, Position = "456" } |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,41 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// OrleansEventNotifierTests.cs
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex Group
|
|
||||
// All rights reserved.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using FakeItEasy; |
|
||||
using Orleans; |
|
||||
using Squidex.Infrastructure.CQRS.Events.Orleans; |
|
||||
using Squidex.Infrastructure.CQRS.Events.Orleans.Grains; |
|
||||
using Xunit; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.CQRS.Events.Grains |
|
||||
{ |
|
||||
public class OrleansEventNotifierTests |
|
||||
{ |
|
||||
private readonly IEventConsumerRegistryGrain registry = A.Fake<IEventConsumerRegistryGrain>(); |
|
||||
private readonly OrleansEventNotifier sut; |
|
||||
|
|
||||
public OrleansEventNotifierTests() |
|
||||
{ |
|
||||
var factory = A.Fake<IGrainFactory>(); |
|
||||
|
|
||||
A.CallTo(() => factory.GetGrain<IEventConsumerRegistryGrain>("Default", null)) |
|
||||
.Returns(registry); |
|
||||
|
|
||||
sut = new OrleansEventNotifier(factory); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_activate_registry_with_stream_name() |
|
||||
{ |
|
||||
sut.NotifyEventsStored("my-stream"); |
|
||||
|
|
||||
A.CallTo(() => registry.ActivateAsync("my-stream")) |
|
||||
.MustHaveHappened(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,108 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// JsonExternalSerializerTests.cs
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex Group
|
|
||||
// All rights reserved.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using FakeItEasy; |
|
||||
using Newtonsoft.Json; |
|
||||
using Orleans.Serialization; |
|
||||
using Xunit; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.Json.Orleans |
|
||||
{ |
|
||||
public class JsonExternalSerializerTests |
|
||||
{ |
|
||||
private readonly JsonExternalSerializer sut = new JsonExternalSerializer(JsonSerializer.CreateDefault()); |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_serialize_js_only() |
|
||||
{ |
|
||||
Assert.True(sut.IsSupportedType(typeof(J<int>))); |
|
||||
Assert.True(sut.IsSupportedType(typeof(J<List<int>>))); |
|
||||
|
|
||||
Assert.False(sut.IsSupportedType(typeof(int))); |
|
||||
Assert.False(sut.IsSupportedType(typeof(List<int>))); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_copy_null() |
|
||||
{ |
|
||||
var v = (string)null; |
|
||||
var c = DeepCopy(v); |
|
||||
|
|
||||
Assert.Null(c); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_copy_null_json() |
|
||||
{ |
|
||||
var v = new J<List<int>>(null); |
|
||||
var c = DeepCopy(v); |
|
||||
|
|
||||
Assert.Null(c.Value); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_not_copy_immutable_values() |
|
||||
{ |
|
||||
var v = new List<int> { 1, 2, 3 }.AsJ(true); |
|
||||
var c = DeepCopy(v); |
|
||||
|
|
||||
Assert.Same(v.Value, c.Value); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_copy_non_immutable_values() |
|
||||
{ |
|
||||
var value = new J<List<int>>(new List<int> { 1, 2, 3 }); |
|
||||
var copy = (J<List<int>>)sut.DeepCopy(value, null); |
|
||||
|
|
||||
Assert.Equal(value.Value, copy.Value); |
|
||||
Assert.NotSame(value.Value, copy.Value); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_serialize_and_deserialize_value() |
|
||||
{ |
|
||||
var value = new J<List<int>>(new List<int> { 1, 2, 3 }); |
|
||||
|
|
||||
var writtenLength = 0; |
|
||||
var writtenBuffer = (byte[])null; |
|
||||
|
|
||||
var writer = A.Fake<IBinaryTokenStreamWriter>(); |
|
||||
var writerContext = new SerializationContext(null) { StreamWriter = writer }; |
|
||||
|
|
||||
A.CallTo(() => writer.Write(A<int>.Ignored)) |
|
||||
.Invokes(new Action<int>(x => writtenLength = x)); |
|
||||
|
|
||||
A.CallTo(() => writer.Write(A<byte[]>.Ignored)) |
|
||||
.Invokes(new Action<byte[]>(x => writtenBuffer = x)); |
|
||||
|
|
||||
sut.Serialize(value, writerContext, value.GetType()); |
|
||||
|
|
||||
var reader = A.Fake<IBinaryTokenStreamReader>(); |
|
||||
var readerContext = new DeserializationContext(null) { StreamReader = reader }; |
|
||||
|
|
||||
A.CallTo(() => reader.ReadInt()) |
|
||||
.Returns(writtenLength); |
|
||||
|
|
||||
A.CallTo(() => reader.ReadBytes(writtenLength)) |
|
||||
.Returns(writtenBuffer); |
|
||||
|
|
||||
var copy = (J<List<int>>)sut.Deserialize(value.GetType(), readerContext); |
|
||||
|
|
||||
Assert.Equal(value.Value, copy.Value); |
|
||||
Assert.NotSame(value.Value, copy.Value); |
|
||||
} |
|
||||
|
|
||||
private T DeepCopy<T>(T value) |
|
||||
{ |
|
||||
return (T)sut.DeepCopy(value, null); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
Loading…
Reference in new issue