mirror of https://github.com/Squidex/squidex.git
Browse Source
* Bring event store back. * Progress with event store3. * Fix offset and position handling.pull/785/head
committed by
GitHub
18 changed files with 491 additions and 322 deletions
@ -0,0 +1,79 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Concurrent; |
|||
using System.Threading.Tasks; |
|||
using EventStore.Client; |
|||
using Squidex.Text; |
|||
|
|||
namespace Squidex.Infrastructure.EventSourcing |
|||
{ |
|||
public sealed class EventStoreProjectionClient |
|||
{ |
|||
private readonly ConcurrentDictionary<string, bool> projections = new ConcurrentDictionary<string, bool>(); |
|||
private readonly string projectionPrefix; |
|||
private readonly EventStoreProjectionManagementClient client; |
|||
|
|||
public EventStoreProjectionClient(EventStoreClientSettings settings, string projectionPrefix) |
|||
{ |
|||
client = new EventStoreProjectionManagementClient(settings); |
|||
|
|||
this.projectionPrefix = projectionPrefix; |
|||
} |
|||
|
|||
private string CreateFilterProjectionName(string filter) |
|||
{ |
|||
return $"by-{projectionPrefix.Slugify()}-{filter.Slugify()}"; |
|||
} |
|||
|
|||
public async Task<string> CreateProjectionAsync(string? streamFilter = null) |
|||
{ |
|||
if (!string.IsNullOrWhiteSpace(streamFilter) && streamFilter[0] != '^') |
|||
{ |
|||
return $"{projectionPrefix}-{streamFilter}"; |
|||
} |
|||
|
|||
streamFilter ??= ".*"; |
|||
|
|||
var name = CreateFilterProjectionName(streamFilter); |
|||
|
|||
var query = |
|||
$@"fromAll()
|
|||
.when({{ |
|||
$any: function (s, e) {{ |
|||
if (e.streamId.indexOf('{projectionPrefix}') === 0 && /{streamFilter}/.test(e.streamId.substring({projectionPrefix.Length + 1}))) {{ |
|||
linkTo('{name}', e); |
|||
}} |
|||
}} |
|||
}});";
|
|||
|
|||
await CreateProjectionAsync(name, query); |
|||
|
|||
return name; |
|||
} |
|||
|
|||
private async Task CreateProjectionAsync(string name, string query) |
|||
{ |
|||
if (projections.TryAdd(name, true)) |
|||
{ |
|||
try |
|||
{ |
|||
await client.CreateContinuousAsync(name, "fromAll().when()"); |
|||
await client.UpdateAsync(name, query, true); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
if (!ex.Is<InvalidOperationException>()) |
|||
{ |
|||
throw; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,142 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Concurrent; |
|||
using System.Linq; |
|||
using System.Net; |
|||
using System.Net.Http; |
|||
using System.Net.Sockets; |
|||
using System.Threading.Tasks; |
|||
using EventStore.ClientAPI; |
|||
using EventStore.ClientAPI.Exceptions; |
|||
using EventStore.ClientAPI.Projections; |
|||
using Squidex.Hosting.Configuration; |
|||
using Squidex.Text; |
|||
|
|||
namespace Squidex.Infrastructure.EventSourcing |
|||
{ |
|||
public sealed class ProjectionClient |
|||
{ |
|||
private readonly ConcurrentDictionary<string, bool> projections = new ConcurrentDictionary<string, bool>(); |
|||
private readonly IEventStoreConnection connection; |
|||
private readonly string projectionPrefix; |
|||
private readonly string projectionHost; |
|||
private ProjectionsManager projectionsManager; |
|||
|
|||
public ProjectionClient(IEventStoreConnection connection, string projectionPrefix, string projectionHost) |
|||
{ |
|||
this.connection = connection; |
|||
|
|||
this.projectionPrefix = projectionPrefix; |
|||
this.projectionHost = projectionHost; |
|||
} |
|||
|
|||
private string CreateFilterProjectionName(string filter) |
|||
{ |
|||
return $"by-{projectionPrefix.Slugify()}-{filter.Slugify()}"; |
|||
} |
|||
|
|||
public async Task<string> CreateProjectionAsync(string? streamFilter = null) |
|||
{ |
|||
streamFilter ??= ".*"; |
|||
|
|||
var name = CreateFilterProjectionName(streamFilter); |
|||
|
|||
var query = |
|||
$@"fromAll()
|
|||
.when({{ |
|||
$any: function (s, e) {{ |
|||
if (e.streamId.indexOf('{projectionPrefix}') === 0 && /{streamFilter}/.test(e.streamId.substring({projectionPrefix.Length + 1}))) {{ |
|||
linkTo('{name}', e); |
|||
}} |
|||
}} |
|||
}});";
|
|||
|
|||
await CreateProjectionAsync(name, query); |
|||
|
|||
return name; |
|||
} |
|||
|
|||
private async Task CreateProjectionAsync(string name, string query) |
|||
{ |
|||
if (projections.TryAdd(name, true)) |
|||
{ |
|||
try |
|||
{ |
|||
var credentials = connection.Settings.DefaultUserCredentials; |
|||
|
|||
await projectionsManager.CreateContinuousAsync(name, query, credentials); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
if (!ex.Is<ProjectionCommandConflictException>()) |
|||
{ |
|||
throw; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
public async Task ConnectAsync() |
|||
{ |
|||
var addressParts = projectionHost.Split(':'); |
|||
|
|||
if (addressParts.Length < 2 || !int.TryParse(addressParts[1], out var port)) |
|||
{ |
|||
port = 2113; |
|||
} |
|||
|
|||
var endpoints = await Dns.GetHostAddressesAsync(addressParts[0]); |
|||
var endpoint = new IPEndPoint(endpoints.First(x => x.AddressFamily == AddressFamily.InterNetwork), port); |
|||
|
|||
async Task ConnectToSchemaAsync(string schema) |
|||
{ |
|||
projectionsManager = |
|||
new ProjectionsManager( |
|||
connection.Settings.Log, endpoint, |
|||
connection.Settings.OperationTimeout, |
|||
null, |
|||
schema); |
|||
|
|||
await projectionsManager.ListAllAsync(connection.Settings.DefaultUserCredentials); |
|||
} |
|||
|
|||
try |
|||
{ |
|||
try |
|||
{ |
|||
await ConnectToSchemaAsync("https"); |
|||
} |
|||
catch (HttpRequestException) |
|||
{ |
|||
await ConnectToSchemaAsync("http"); |
|||
} |
|||
catch (AggregateException ex) when (ex.Flatten().InnerException is HttpRequestException) |
|||
{ |
|||
await ConnectToSchemaAsync("http"); |
|||
} |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
var error = new ConfigurationError($"GetEventStore cannot connect to event store projections: {projectionHost}."); |
|||
|
|||
throw new ConfigurationException(error, ex); |
|||
} |
|||
} |
|||
|
|||
public static long? ParsePositionOrNull(string? position) |
|||
{ |
|||
return long.TryParse(position, out var parsedPosition) ? (long?)parsedPosition : null; |
|||
} |
|||
|
|||
public static long ParsePosition(string? position) |
|||
{ |
|||
return long.TryParse(position, out var parsedPosition) ? parsedPosition + 1 : StreamPosition.Start; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,83 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Generic; |
|||
using System.Globalization; |
|||
using System.Runtime.CompilerServices; |
|||
using System.Threading; |
|||
using EventStore.Client; |
|||
|
|||
namespace Squidex.Infrastructure.EventSourcing |
|||
{ |
|||
public static class Utils |
|||
{ |
|||
public static StreamRevision ToRevision(this long version) |
|||
{ |
|||
return StreamRevision.FromInt64(version); |
|||
} |
|||
|
|||
public static StreamPosition ToPosition(this long version) |
|||
{ |
|||
if (version <= 0) |
|||
{ |
|||
return StreamPosition.Start; |
|||
} |
|||
|
|||
return StreamPosition.FromInt64(version); |
|||
} |
|||
|
|||
public static StreamPosition ToPosition(this string? position, bool inclusive) |
|||
{ |
|||
if (string.IsNullOrWhiteSpace(position)) |
|||
{ |
|||
return StreamPosition.Start; |
|||
} |
|||
|
|||
if (long.TryParse(position, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedPosition)) |
|||
{ |
|||
if (!inclusive) |
|||
{ |
|||
parsedPosition++; |
|||
} |
|||
|
|||
return StreamPosition.FromInt64(parsedPosition); |
|||
} |
|||
|
|||
return StreamPosition.Start; |
|||
} |
|||
|
|||
public static async IAsyncEnumerable<StoredEvent> IgnoreNotFound(this IAsyncEnumerable<StoredEvent> source, |
|||
[EnumeratorCancellation] CancellationToken ct = default) |
|||
{ |
|||
var enumerator = source.GetAsyncEnumerator(ct); |
|||
|
|||
bool resultFound; |
|||
try |
|||
{ |
|||
resultFound = await enumerator.MoveNextAsync(ct); |
|||
} |
|||
catch (StreamNotFoundException) |
|||
{ |
|||
resultFound = false; |
|||
} |
|||
|
|||
if (!resultFound) |
|||
{ |
|||
yield break; |
|||
} |
|||
|
|||
yield return enumerator.Current; |
|||
|
|||
while (await enumerator.MoveNextAsync(ct)) |
|||
{ |
|||
ct.ThrowIfCancellationRequested(); |
|||
|
|||
yield return enumerator.Current; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,51 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using EventStore.Client; |
|||
using Squidex.Infrastructure.TestHelpers; |
|||
|
|||
namespace Squidex.Infrastructure.EventSourcing |
|||
{ |
|||
public sealed class GetEventStoreFixture : IDisposable |
|||
{ |
|||
private readonly EventStoreClientSettings settings; |
|||
|
|||
public GetEventStore EventStore { get; } |
|||
|
|||
public GetEventStoreFixture() |
|||
{ |
|||
AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); |
|||
|
|||
settings = EventStoreClientSettings.Create("esdb://admin:changeit@127.0.0.1:2113?tls=false"); |
|||
|
|||
EventStore = new GetEventStore(settings, TestUtils.DefaultSerializer); |
|||
EventStore.InitializeAsync(default).Wait(); |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
CleanupAsync().Wait(); |
|||
} |
|||
|
|||
private async Task CleanupAsync() |
|||
{ |
|||
var projectionsManager = new EventStoreProjectionManagementClient(settings); |
|||
|
|||
await foreach (var projection in projectionsManager.ListAllAsync()) |
|||
{ |
|||
var name = projection.Name; |
|||
|
|||
if (name.StartsWith("by-squidex-test", StringComparison.OrdinalIgnoreCase)) |
|||
{ |
|||
await projectionsManager.DisableAsync(name); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Xunit; |
|||
|
|||
#pragma warning disable SA1300 // Element should begin with upper-case letter
|
|||
|
|||
namespace Squidex.Infrastructure.EventSourcing |
|||
{ |
|||
[Trait("Category", "Dependencies")] |
|||
public class GetEventStoreTests : EventStoreTests<GetEventStore>, IClassFixture<GetEventStoreFixture> |
|||
{ |
|||
public GetEventStoreFixture _ { get; } |
|||
|
|||
protected override int SubscriptionDelayInMs { get; } = 1000; |
|||
|
|||
public GetEventStoreTests(GetEventStoreFixture fixture) |
|||
{ |
|||
_ = fixture; |
|||
} |
|||
|
|||
public override GetEventStore CreateStore() |
|||
{ |
|||
return _.EventStore; |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue