mirror of https://github.com/Squidex/squidex.git
35 changed files with 662 additions and 243 deletions
@ -1,11 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Squidex.Infrastructure.MongoDb.EventStore |
|||
{ |
|||
public class MongoStreamsRepository |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
// ==========================================================================
|
|||
// InfrastructureErrors.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using Microsoft.Extensions.Logging; |
|||
|
|||
namespace Squidex.Infrastructure.Redis |
|||
{ |
|||
public class InfrastructureErrors |
|||
{ |
|||
public static readonly EventId InvalidatingReceivedFailed = new EventId(10001, "InvalidingReceivedFailed"); |
|||
|
|||
public static readonly EventId InvalidatingPublishedFailed = new EventId(10002, "InvalidatingPublishedFailed"); |
|||
} |
|||
} |
|||
@ -0,0 +1,63 @@ |
|||
// ==========================================================================
|
|||
// RedisEventNotifier.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using Microsoft.Extensions.Logging; |
|||
using Squidex.Infrastructure.CQRS.Events; |
|||
using StackExchange.Redis; |
|||
|
|||
namespace Squidex.Infrastructure.Redis |
|||
{ |
|||
public sealed class RedisEventNotifier : IEventNotifier |
|||
{ |
|||
private const string Channel = "SquidexEventNotifications"; |
|||
private readonly InMemoryEventNotifier inMemoryNotifier = new InMemoryEventNotifier(); |
|||
private readonly ISubscriber subscriber; |
|||
private readonly ILogger<RedisEventNotifier> logger; |
|||
|
|||
public RedisEventNotifier(IConnectionMultiplexer redis, ILogger<RedisEventNotifier> logger) |
|||
{ |
|||
Guard.NotNull(redis, nameof(redis)); |
|||
Guard.NotNull(logger, nameof(logger)); |
|||
|
|||
subscriber = redis.GetSubscriber(); |
|||
subscriber.Subscribe(Channel, (channel, value) => HandleInvalidation()); |
|||
|
|||
this.logger = logger; |
|||
} |
|||
|
|||
private void HandleInvalidation() |
|||
{ |
|||
try |
|||
{ |
|||
inMemoryNotifier.NotifyEventsStored(); |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
logger.LogError(InfrastructureErrors.InvalidatingReceivedFailed, e, "Failed to receive invalidation message."); |
|||
} |
|||
} |
|||
|
|||
public void NotifyEventsStored() |
|||
{ |
|||
try |
|||
{ |
|||
subscriber.Publish(Channel, RedisValue.Null); |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
logger.LogError(InfrastructureErrors.InvalidatingReceivedFailed, e, "Failed to send invalidation message"); |
|||
} |
|||
} |
|||
|
|||
public void Subscribe(Action handler) |
|||
{ |
|||
inMemoryNotifier.Subscribe(handler); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
// ==========================================================================
|
|||
// RedisExternalSystem.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using StackExchange.Redis; |
|||
|
|||
namespace Squidex.Infrastructure.Redis |
|||
{ |
|||
public class RedisExternalSystem |
|||
{ |
|||
private readonly IConnectionMultiplexer redis; |
|||
|
|||
public RedisExternalSystem(IConnectionMultiplexer redis) |
|||
{ |
|||
Guard.NotNull(redis, nameof(redis)); |
|||
|
|||
this.redis = redis; |
|||
} |
|||
|
|||
public void CheckConnection() |
|||
{ |
|||
try |
|||
{ |
|||
redis.GetStatus(); |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
throw new ConfigurationException($"Redis connection failed to connect to database {redis.Configuration}", e); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,56 @@ |
|||
// ==========================================================================
|
|||
// RedisInvalidatingCache.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using Microsoft.Extensions.Caching.Memory; |
|||
using Microsoft.Extensions.Logging; |
|||
using StackExchange.Redis; |
|||
|
|||
namespace Squidex.Infrastructure.Redis |
|||
{ |
|||
public class RedisInvalidatingCache : IMemoryCache |
|||
{ |
|||
private readonly IMemoryCache inner; |
|||
private readonly RedisInvalidator invalidator; |
|||
|
|||
public RedisInvalidatingCache(IMemoryCache inner, IConnectionMultiplexer redis, ILogger<RedisInvalidatingCache> logger) |
|||
{ |
|||
Guard.NotNull(redis, nameof(redis)); |
|||
Guard.NotNull(inner, nameof(inner)); |
|||
Guard.NotNull(logger, nameof(logger)); |
|||
|
|||
this.inner = inner; |
|||
|
|||
invalidator = new RedisInvalidator(redis, inner, logger); |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
inner.Dispose(); |
|||
} |
|||
|
|||
public bool TryGetValue(object key, out object value) |
|||
{ |
|||
return inner.TryGetValue(key, out value); |
|||
} |
|||
|
|||
public void Remove(object key) |
|||
{ |
|||
inner.Remove(key); |
|||
|
|||
if (key is string) |
|||
{ |
|||
invalidator.Invalidate(key.ToString()); |
|||
} |
|||
} |
|||
|
|||
public ICacheEntry CreateEntry(object key) |
|||
{ |
|||
return new WrapperCacheEntry(inner.CreateEntry(key), invalidator); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,96 @@ |
|||
// ==========================================================================
|
|||
// RedisInvalidator.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using Microsoft.Extensions.Caching.Memory; |
|||
using Microsoft.Extensions.Logging; |
|||
using StackExchange.Redis; |
|||
|
|||
// ReSharper disable InvertIf
|
|||
// ReSharper disable ArrangeThisQualifier
|
|||
|
|||
namespace Squidex.Infrastructure.Redis |
|||
{ |
|||
internal sealed class RedisInvalidator |
|||
{ |
|||
private const string Channel = "SquidexChannelInvalidations"; |
|||
private readonly Guid instanceId = Guid.NewGuid(); |
|||
private readonly ISubscriber subscriber; |
|||
private readonly IMemoryCache cache; |
|||
private readonly ILogger<RedisInvalidatingCache> logger; |
|||
private int invalidationsReceived; |
|||
|
|||
public int InvalidationsReceived |
|||
{ |
|||
get |
|||
{ |
|||
return invalidationsReceived; |
|||
} |
|||
} |
|||
|
|||
public RedisInvalidator(IConnectionMultiplexer redis, IMemoryCache cache, ILogger<RedisInvalidatingCache> logger) |
|||
{ |
|||
this.cache = cache; |
|||
|
|||
subscriber = redis.GetSubscriber(); |
|||
subscriber.Subscribe(Channel, (channel, value) => HandleInvalidation(value)); |
|||
|
|||
this.logger = logger; |
|||
} |
|||
|
|||
private void HandleInvalidation(string value) |
|||
{ |
|||
try |
|||
{ |
|||
if (string.IsNullOrWhiteSpace(value)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var parts = value.Split('#'); |
|||
|
|||
if (parts.Length != 2) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
Guid sender; |
|||
|
|||
if (!Guid.TryParse(parts[0], out sender)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (sender != instanceId) |
|||
{ |
|||
invalidationsReceived++; |
|||
|
|||
cache.Remove(parts[1]); |
|||
} |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
logger.LogError(InfrastructureErrors.InvalidatingReceivedFailed, e, "Failed to receive invalidation message."); |
|||
} |
|||
} |
|||
|
|||
public void Invalidate(string key) |
|||
{ |
|||
try |
|||
{ |
|||
var message = string.Join("#", instanceId.ToString()); |
|||
|
|||
subscriber.Publish(Channel, message); |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
logger.LogError(InfrastructureErrors.InvalidatingReceivedFailed, e, "Failed to send invalidation message {0}", key); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
|||
<PropertyGroup> |
|||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion> |
|||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath> |
|||
</PropertyGroup> |
|||
|
|||
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" /> |
|||
<PropertyGroup Label="Globals"> |
|||
<ProjectGuid>d7166c56-178a-4457-b56a-c615c7450dee</ProjectGuid> |
|||
<RootNamespace>Squidex.Infrastructure.Redis</RootNamespace> |
|||
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath> |
|||
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath> |
|||
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion> |
|||
</PropertyGroup> |
|||
|
|||
<PropertyGroup> |
|||
<SchemaVersion>2.0</SchemaVersion> |
|||
</PropertyGroup> |
|||
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" /> |
|||
</Project> |
|||
@ -0,0 +1,83 @@ |
|||
// ==========================================================================
|
|||
// WrapperCacheEntry.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Microsoft.Extensions.Caching.Memory; |
|||
using Microsoft.Extensions.Primitives; |
|||
|
|||
namespace Squidex.Infrastructure.Redis |
|||
{ |
|||
internal sealed class WrapperCacheEntry : ICacheEntry |
|||
{ |
|||
private readonly ICacheEntry inner; |
|||
private readonly RedisInvalidator invalidator; |
|||
|
|||
public object Key |
|||
{ |
|||
get { return inner.Key; } |
|||
} |
|||
|
|||
public IList<IChangeToken> ExpirationTokens |
|||
{ |
|||
get { return inner.ExpirationTokens; } |
|||
} |
|||
|
|||
public IList<PostEvictionCallbackRegistration> PostEvictionCallbacks |
|||
{ |
|||
get { return inner.PostEvictionCallbacks; } |
|||
} |
|||
|
|||
public DateTimeOffset? AbsoluteExpiration |
|||
{ |
|||
get { return inner.AbsoluteExpiration; } |
|||
set { inner.AbsoluteExpiration = value; } |
|||
} |
|||
|
|||
public TimeSpan? AbsoluteExpirationRelativeToNow |
|||
{ |
|||
get { return inner.AbsoluteExpirationRelativeToNow; } |
|||
set { inner.AbsoluteExpirationRelativeToNow = value; } |
|||
} |
|||
|
|||
public TimeSpan? SlidingExpiration |
|||
{ |
|||
get { return inner.SlidingExpiration; } |
|||
set { inner.SlidingExpiration = value; } |
|||
} |
|||
|
|||
public CacheItemPriority Priority |
|||
{ |
|||
get { return inner.Priority; } |
|||
set { inner.Priority = value; } |
|||
} |
|||
|
|||
public object Value |
|||
{ |
|||
get { return inner.Value; } |
|||
set { inner.Value = value; } |
|||
} |
|||
|
|||
public WrapperCacheEntry(ICacheEntry inner, RedisInvalidator invalidator) |
|||
{ |
|||
this.inner = inner; |
|||
|
|||
this.invalidator = invalidator; |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
if (Key is string) |
|||
{ |
|||
invalidator.Invalidate(Key?.ToString()); |
|||
} |
|||
|
|||
inner.Dispose(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
{ |
|||
"version": "1.0.0-*", |
|||
"dependencies": { |
|||
"Autofac": "4.3.0", |
|||
"Microsoft.Extensions.Caching.Abstractions": "1.1.0", |
|||
"Microsoft.Extensions.Logging": "1.1.0", |
|||
"Newtonsoft.Json": "9.0.2-beta2", |
|||
"NodaTime": "2.0.0-beta20170123", |
|||
"Squidex.Infrastructure": "1.0.0-*", |
|||
"StackExchange.Redis.StrongName": "1.2.0", |
|||
"System.Linq": "4.3.0", |
|||
"System.Reactive": "3.1.1", |
|||
"System.Reflection.TypeExtensions": "4.3.0", |
|||
"System.Security.Claims": "4.3.0" |
|||
}, |
|||
"frameworks": { |
|||
"netstandard1.6": { |
|||
"dependencies": { |
|||
"NETStandard.Library": "1.6.1" |
|||
}, |
|||
"imports": "dnxcore50" |
|||
} |
|||
}, |
|||
"tooling": { |
|||
"defaultNamespace": "Squidex.Infrastructure.Redis" |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
// ==========================================================================
|
|||
// InMemoryEventNotifier.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Reactive.Subjects; |
|||
|
|||
namespace Squidex.Infrastructure.CQRS.Events |
|||
{ |
|||
public sealed class InMemoryEventNotifier : IEventNotifier |
|||
{ |
|||
private readonly Subject<object> subject = new Subject<object>(); |
|||
|
|||
public void NotifyEventsStored() |
|||
{ |
|||
subject.OnNext(null); |
|||
} |
|||
|
|||
public void Subscribe(Action handler) |
|||
{ |
|||
subject.Subscribe(_ => handler()); |
|||
} |
|||
} |
|||
} |
|||
@ -1,38 +0,0 @@ |
|||
// ==========================================================================
|
|||
// MongoDbStoresExternalSystem.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using MongoDB.Driver; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Read.MongoDb |
|||
{ |
|||
public sealed class MongoDbStoresExternalSystem : IExternalSystem |
|||
{ |
|||
private readonly IMongoDatabase database; |
|||
|
|||
public MongoDbStoresExternalSystem(IMongoDatabase database) |
|||
{ |
|||
Guard.NotNull(database, nameof(database)); |
|||
|
|||
this.database = database; |
|||
} |
|||
|
|||
public void CheckConnection() |
|||
{ |
|||
try |
|||
{ |
|||
database.ListCollections(); |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
throw new ConfigurationException($"MongoDb Event Store failed to connect to database {database.DatabaseNamespace.DatabaseName}", e); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,87 @@ |
|||
// ==========================================================================
|
|||
// ClusterModule.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using Autofac; |
|||
using Microsoft.Extensions.Caching.Memory; |
|||
using Microsoft.Extensions.Configuration; |
|||
using Microsoft.Extensions.Logging; |
|||
using Microsoft.Extensions.Options; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.CQRS.Events; |
|||
using Squidex.Infrastructure.Redis; |
|||
using StackExchange.Redis; |
|||
|
|||
namespace Squidex.Config.Domain |
|||
{ |
|||
public class ClusterModule : Module |
|||
{ |
|||
public IConfiguration Configuration { get; } |
|||
|
|||
public ClusterModule(IConfiguration configuration) |
|||
{ |
|||
Configuration = configuration; |
|||
} |
|||
|
|||
protected override void Load(ContainerBuilder builder) |
|||
{ |
|||
var canCatch = Configuration.GetValue<bool>("squidex:catch"); |
|||
|
|||
if (canCatch) |
|||
{ |
|||
builder.RegisterType<EventReceiver>() |
|||
.AsSelf() |
|||
.InstancePerDependency(); |
|||
} |
|||
|
|||
var clustererType = Configuration.GetValue<string>("squidex:clusterer:type"); |
|||
|
|||
if (string.IsNullOrWhiteSpace(clustererType)) |
|||
{ |
|||
throw new ConfigurationException("You must specify the clusterer type in the 'squidex:clusterer:type' configuration section."); |
|||
} |
|||
|
|||
if (string.Equals(clustererType, "Slack", StringComparison.OrdinalIgnoreCase)) |
|||
{ |
|||
var connectionString = Configuration.GetValue<string>("squidex:clusterer:redis:connectionString"); |
|||
|
|||
if (string.IsNullOrWhiteSpace(connectionString) || !Uri.IsWellFormedUriString(connectionString, UriKind.Absolute)) |
|||
{ |
|||
throw new ConfigurationException("You must specify the Redis connection string in the 'squidex:clusterer:redis:connectionString' configuration section."); |
|||
} |
|||
|
|||
builder.Register(c => ConnectionMultiplexer.Connect(connectionString)) |
|||
.As<IConnectionMultiplexer>() |
|||
.SingleInstance(); |
|||
|
|||
builder.RegisterType<RedisEventNotifier>() |
|||
.As<IEventNotifier>() |
|||
.SingleInstance(); |
|||
|
|||
builder.RegisterType<RedisExternalSystem>() |
|||
.As<IExternalSystem>() |
|||
.SingleInstance(); |
|||
|
|||
builder.Register(c => |
|||
{ |
|||
var inner = new MemoryCache(c.Resolve<IOptions<MemoryCacheOptions>>()); |
|||
|
|||
return new RedisInvalidatingCache(inner, |
|||
c.Resolve<IConnectionMultiplexer>(), |
|||
c.Resolve<ILogger<RedisInvalidatingCache>>()); |
|||
}) |
|||
.As<IMemoryCache>() |
|||
.SingleInstance(); |
|||
} |
|||
else |
|||
{ |
|||
throw new ConfigurationException($"Unsupported clusterer type '{clustererType}' for key 'squidex:clusterer:type', supported: Redis."); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,81 +0,0 @@ |
|||
// ==========================================================================
|
|||
// EventStoreModule.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using Autofac; |
|||
using Microsoft.Extensions.Configuration; |
|||
using RabbitMQ.Client; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.CQRS.Events; |
|||
using Squidex.Infrastructure.RabbitMq; |
|||
|
|||
namespace Squidex.Config.Domain |
|||
{ |
|||
public class EventBusModule : Module |
|||
{ |
|||
public IConfiguration Configuration { get; } |
|||
|
|||
public EventBusModule(IConfiguration configuration) |
|||
{ |
|||
Configuration = configuration; |
|||
} |
|||
|
|||
protected override void Load(ContainerBuilder builder) |
|||
{ |
|||
var eventBusType = Configuration.GetValue<string>("squidex:eventBus:type"); |
|||
|
|||
if (string.IsNullOrWhiteSpace(eventBusType)) |
|||
{ |
|||
throw new ConfigurationException("You must specify the event bus type in the 'squidex:eventBus:type' configuration section."); |
|||
} |
|||
|
|||
var canCatch = Configuration.GetValue<bool>("squidex:eventBus:catch"); |
|||
|
|||
builder.RegisterType<EventReceiver>() |
|||
.WithParameter(new NamedParameter("canCatch", canCatch)) |
|||
.AsSelf() |
|||
.SingleInstance(); |
|||
|
|||
if (string.Equals(eventBusType, "Memory", StringComparison.OrdinalIgnoreCase)) |
|||
{ |
|||
builder.RegisterType<InMemoryEventBus>() |
|||
.As<IEventStream>() |
|||
.As<IEventPublisher>() |
|||
.SingleInstance(); |
|||
} |
|||
else if (string.Equals(eventBusType, "RabbitMq", StringComparison.OrdinalIgnoreCase)) |
|||
{ |
|||
var connectionString = Configuration.GetValue<string>("squidex:eventBus:rabbitMq:connectionString"); |
|||
|
|||
if (string.IsNullOrWhiteSpace(connectionString) || !Uri.IsWellFormedUriString(connectionString, UriKind.Absolute)) |
|||
{ |
|||
throw new ConfigurationException("You must specify the RabbitMq connection string in the 'squidex:eventBus:rabbitMq:connectionString' configuration section."); |
|||
} |
|||
|
|||
var queueName = Configuration.GetValue<string>("squidex:eventBus:rabbitMq:queueName"); |
|||
|
|||
builder.Register(c => |
|||
{ |
|||
var connectionFactory = new ConnectionFactory(); |
|||
|
|||
connectionFactory.SetUri(new Uri(connectionString)); |
|||
|
|||
return new RabbitMqEventBus(connectionFactory, canCatch, queueName); |
|||
}) |
|||
.As<IEventStream>() |
|||
.As<IEventPublisher>() |
|||
.As<IExternalSystem>() |
|||
.SingleInstance(); |
|||
} |
|||
else |
|||
{ |
|||
throw new ConfigurationException($"Unsupported store type '{eventBusType}' for key 'squidex:eventStore:type', supported: Memory, RabbmitMq."); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue