mirror of https://github.com/Squidex/squidex.git
10 changed files with 273 additions and 53 deletions
@ -0,0 +1,16 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Infrastructure.EventSourcing |
||||
|
{ |
||||
|
internal static class Constants |
||||
|
{ |
||||
|
public const string Collection = "Events"; |
||||
|
|
||||
|
public const string LeaseCollection = "Leases"; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,150 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Text.RegularExpressions; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.Azure.Documents; |
||||
|
using Microsoft.Azure.Documents.ChangeFeedProcessor.FeedProcessing; |
||||
|
using Newtonsoft.Json; |
||||
|
using Builder = Microsoft.Azure.Documents.ChangeFeedProcessor.ChangeFeedProcessorBuilder; |
||||
|
using Collection = Microsoft.Azure.Documents.ChangeFeedProcessor.DocumentCollectionInfo; |
||||
|
using Options = Microsoft.Azure.Documents.ChangeFeedProcessor.ChangeFeedProcessorOptions; |
||||
|
|
||||
|
#pragma warning disable IDE0017 // Simplify object initialization
|
||||
|
|
||||
|
namespace Squidex.Infrastructure.EventSourcing |
||||
|
{ |
||||
|
internal sealed class CosmosDbSubscription : IEventSubscription, IChangeFeedObserverFactory, IChangeFeedObserver |
||||
|
{ |
||||
|
private readonly TaskCompletionSource<bool> processorStopRequested = new TaskCompletionSource<bool>(); |
||||
|
private readonly Task processorTask; |
||||
|
private readonly CosmosDbEventStore store; |
||||
|
private readonly Regex regex; |
||||
|
private readonly string hostName; |
||||
|
private readonly IEventSubscriber subscriber; |
||||
|
|
||||
|
public CosmosDbSubscription(CosmosDbEventStore store, IEventSubscriber subscriber, string streamFilter, string position = null) |
||||
|
{ |
||||
|
var fromBeginning = string.IsNullOrWhiteSpace(position); |
||||
|
|
||||
|
if (fromBeginning) |
||||
|
{ |
||||
|
hostName = $"squidex.{DateTime.UtcNow.Ticks.ToString()}"; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
hostName = position; |
||||
|
} |
||||
|
|
||||
|
if (!StreamFilter.IsAll(streamFilter)) |
||||
|
{ |
||||
|
regex = new Regex(streamFilter); |
||||
|
} |
||||
|
|
||||
|
this.store = store; |
||||
|
|
||||
|
this.subscriber = subscriber; |
||||
|
|
||||
|
processorTask = Task.Run(async () => |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
Collection CreateCollection(string name) |
||||
|
{ |
||||
|
var collection = new Collection(); |
||||
|
|
||||
|
collection.CollectionName = name; |
||||
|
collection.DatabaseName = store.DatabaseId; |
||||
|
collection.MasterKey = store.MasterKey; |
||||
|
collection.Uri = store.ServiceUri; |
||||
|
|
||||
|
return collection; |
||||
|
} |
||||
|
|
||||
|
var processor = |
||||
|
await new Builder() |
||||
|
.WithFeedCollection(CreateCollection(Constants.Collection)) |
||||
|
.WithLeaseCollection(CreateCollection(Constants.LeaseCollection)) |
||||
|
.WithHostName(hostName) |
||||
|
.WithProcessorOptions(new Options { StartFromBeginning = fromBeginning }) |
||||
|
.WithObserverFactory(this) |
||||
|
.BuildAsync(); |
||||
|
|
||||
|
await processor.StartAsync(); |
||||
|
await processorStopRequested.Task; |
||||
|
await processor.StopAsync(); |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
await subscriber.OnErrorAsync(this, ex); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public IChangeFeedObserver CreateObserver() |
||||
|
{ |
||||
|
return this; |
||||
|
} |
||||
|
|
||||
|
public async Task CloseAsync(IChangeFeedObserverContext context, ChangeFeedObserverCloseReason reason) |
||||
|
{ |
||||
|
if (reason == ChangeFeedObserverCloseReason.ObserverError) |
||||
|
{ |
||||
|
await subscriber.OnErrorAsync(this, new InvalidOperationException("Change feed observer failed.")); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public Task OpenAsync(IChangeFeedObserverContext context) |
||||
|
{ |
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
public async Task ProcessChangesAsync(IChangeFeedObserverContext context, IReadOnlyList<Document> docs, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
if (!processorStopRequested.Task.IsCompleted) |
||||
|
{ |
||||
|
foreach (var document in docs) |
||||
|
{ |
||||
|
if (!processorStopRequested.Task.IsCompleted) |
||||
|
{ |
||||
|
var streamName = document.GetPropertyValue<string>("eventStream"); |
||||
|
|
||||
|
if (regex == null || regex.IsMatch(streamName)) |
||||
|
{ |
||||
|
var commit = JsonConvert.DeserializeObject<CosmosDbEventCommit>(document.ToString(), store.SerializerSettings); |
||||
|
|
||||
|
var eventStreamOffset = (int)commit.EventStreamOffset; |
||||
|
|
||||
|
foreach (var @event in commit.Events) |
||||
|
{ |
||||
|
eventStreamOffset++; |
||||
|
|
||||
|
var eventData = @event.ToEventData(); |
||||
|
|
||||
|
await subscriber.OnEventAsync(this, new StoredEvent(commit.EventStream, hostName, eventStreamOffset, eventData)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void WakeUp() |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public Task StopAsync() |
||||
|
{ |
||||
|
processorStopRequested.SetResult(true); |
||||
|
|
||||
|
return processorTask; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.EventSourcing |
||||
|
{ |
||||
|
public static class StreamFilter |
||||
|
{ |
||||
|
public static bool IsAll(string filter) |
||||
|
{ |
||||
|
return string.IsNullOrWhiteSpace(filter) |
||||
|
|| string.Equals(filter, ".*", StringComparison.OrdinalIgnoreCase) |
||||
|
|| string.Equals(filter, "(.*)", StringComparison.OrdinalIgnoreCase) |
||||
|
|| string.Equals(filter, "(.*?)", StringComparison.OrdinalIgnoreCase); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue