mirror of https://github.com/Squidex/squidex.git
Browse Source
* Sharding V1 * Revert naming. * Revert more changes. * Fix text index. * Full text improvement. * Improved full text. * Update names. * Revert search.pull/1056/head
committed by
GitHub
51 changed files with 1651 additions and 498 deletions
@ -0,0 +1,90 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using MongoDB.Driver; |
|||
using Squidex.Domain.Apps.Core.Assets; |
|||
using Squidex.Domain.Apps.Entities.Assets.Repositories; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.States; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.MongoDb.Assets; |
|||
|
|||
public sealed class MongoShardedAssetRepository : ShardedSnapshotStore<MongoAssetRepository, Asset>, IAssetRepository |
|||
{ |
|||
public MongoShardedAssetRepository(IShardingStrategy sharding, Func<string, MongoAssetRepository> factory) |
|||
: base(sharding, factory) |
|||
{ |
|||
} |
|||
|
|||
public IEnumerable<IMongoCollection<MongoAssetEntity>> GetInternalCollections() |
|||
{ |
|||
return Shards.Select(x => x.GetInternalCollection()); |
|||
} |
|||
|
|||
public async Task<Asset?> FindAssetAsync(DomainId id, |
|||
CancellationToken ct = default) |
|||
{ |
|||
Asset? result = null; |
|||
|
|||
foreach (var shard in Shards) |
|||
{ |
|||
if ((result = await shard.FindAssetAsync(id, ct)) != null) |
|||
{ |
|||
return result; |
|||
} |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
public Task<Asset?> FindAssetAsync(DomainId appId, DomainId id, bool allowDeleted, |
|||
CancellationToken ct = default) |
|||
{ |
|||
return Shard(appId).FindAssetAsync(appId, id, allowDeleted, ct); |
|||
} |
|||
|
|||
public Task<Asset?> FindAssetByHashAsync(DomainId appId, string hash, string fileName, long fileSize, |
|||
CancellationToken ct = default) |
|||
{ |
|||
return Shard(appId).FindAssetByHashAsync(appId, hash, fileName, fileSize, ct); |
|||
} |
|||
|
|||
public Task<Asset?> FindAssetBySlugAsync(DomainId appId, string slug, bool allowDeleted, |
|||
CancellationToken ct = default) |
|||
{ |
|||
return Shard(appId).FindAssetBySlugAsync(appId, slug, allowDeleted, ct); |
|||
} |
|||
|
|||
public Task<IResultList<Asset>> QueryAsync(DomainId appId, DomainId? parentId, Q q, |
|||
CancellationToken ct = default) |
|||
{ |
|||
return Shard(appId).QueryAsync(appId, parentId, q, ct); |
|||
} |
|||
|
|||
public Task<IReadOnlyList<DomainId>> QueryChildIdsAsync(DomainId appId, DomainId parentId, |
|||
CancellationToken ct = default) |
|||
{ |
|||
return Shard(appId).QueryChildIdsAsync(appId, parentId, ct); |
|||
} |
|||
|
|||
public Task<IReadOnlyList<DomainId>> QueryIdsAsync(DomainId appId, HashSet<DomainId> ids, |
|||
CancellationToken ct = default) |
|||
{ |
|||
return Shard(appId).QueryIdsAsync(appId, ids, ct); |
|||
} |
|||
|
|||
public IAsyncEnumerable<Asset> StreamAll(DomainId appId, |
|||
CancellationToken ct = default) |
|||
{ |
|||
return Shard(appId).StreamAll(appId, ct); |
|||
} |
|||
|
|||
protected override string GetShardKey(Asset state) |
|||
{ |
|||
return GetShardKey(state.AppId.Id); |
|||
} |
|||
} |
|||
@ -0,0 +1,183 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Text; |
|||
using MongoDB.Bson.Serialization; |
|||
using MongoDB.Bson.Serialization.Serializers; |
|||
using Squidex.Domain.Apps.Entities.Contents.Text; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.MongoDb; |
|||
|
|||
public sealed class BsonUniqueContentIdSerializer : SerializerBase<UniqueContentId> |
|||
{ |
|||
private const byte GuidLength = 16; |
|||
private static readonly BsonUniqueContentIdSerializer Instance = new BsonUniqueContentIdSerializer(); |
|||
|
|||
public static void Register() |
|||
{ |
|||
BsonSerializer.TryRegisterSerializer(Instance); |
|||
} |
|||
|
|||
private BsonUniqueContentIdSerializer() |
|||
{ |
|||
} |
|||
|
|||
public static UniqueContentId NextAppId(DomainId appId) |
|||
{ |
|||
static void IncrementByteArray(byte[] bytes) |
|||
{ |
|||
for (var i = 0; i < bytes.Length; i++) |
|||
{ |
|||
var value = bytes[i]; |
|||
if (value < byte.MaxValue) |
|||
{ |
|||
value += 1; |
|||
bytes[i] = value; |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
|
|||
if (Guid.TryParse(appId.ToString(), out var id)) |
|||
{ |
|||
var bytes = id.ToByteArray(); |
|||
|
|||
IncrementByteArray(bytes); |
|||
|
|||
return new UniqueContentId(DomainId.Create(new Guid(bytes)), DomainId.Empty); |
|||
} |
|||
else |
|||
{ |
|||
var bytes = Encoding.UTF8.GetBytes(appId.ToString()); |
|||
|
|||
IncrementByteArray(bytes); |
|||
|
|||
return new UniqueContentId(DomainId.Create(Encoding.UTF8.GetString(bytes)), DomainId.Empty); |
|||
} |
|||
} |
|||
|
|||
public override UniqueContentId Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) |
|||
{ |
|||
var buffer = context.Reader.ReadBytes()!; |
|||
var offset = 0; |
|||
|
|||
static DomainId ReadId(byte[] buffer, ref int offset) |
|||
{ |
|||
DomainId id; |
|||
|
|||
// If we have reached the end of the buffer then
|
|||
if (offset >= buffer.Length) |
|||
{ |
|||
return default; |
|||
} |
|||
|
|||
var length = buffer[offset++]; |
|||
// Special length indicator for all guids.
|
|||
if (length == 0xFF) |
|||
{ |
|||
id = DomainId.Create(new Guid(buffer.AsSpan(offset, GuidLength))); |
|||
offset += GuidLength; |
|||
} |
|||
else |
|||
{ |
|||
id = DomainId.Create(Encoding.UTF8.GetString(buffer.AsSpan(offset, length))); |
|||
offset += length; |
|||
} |
|||
|
|||
return id; |
|||
} |
|||
|
|||
return new UniqueContentId(ReadId(buffer, ref offset), ReadId(buffer, ref offset)); |
|||
} |
|||
|
|||
public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, UniqueContentId value) |
|||
{ |
|||
var appId = CheckId(value.AppId); |
|||
|
|||
var contentId = CheckId(value.ContentId); |
|||
|
|||
var isEmptyContentId = |
|||
contentId.IsGuid && |
|||
contentId.Guid == default; |
|||
|
|||
// Do not write empty Ids to the buffer to allow prefix searches.
|
|||
var contentLength = !isEmptyContentId ? contentId.Length + 1 : 0; |
|||
|
|||
var bufferLength = appId.Length + 1 + contentLength; |
|||
var bufferArray = new byte[bufferLength]; |
|||
|
|||
var offset = Write(bufferArray, 0, |
|||
appId.IsGuid, |
|||
appId.Guid, |
|||
appId.Source, |
|||
appId.Length); |
|||
|
|||
if (!isEmptyContentId) |
|||
{ |
|||
// Do not write the empty content id, so we can search for app as well.
|
|||
Write(bufferArray, offset, |
|||
contentId.IsGuid, |
|||
contentId.Guid, |
|||
contentId.Source, |
|||
contentId.Length); |
|||
} |
|||
|
|||
static int Write(byte[] buffer, int offset, bool isGuid, Guid guid, string id, int idLength) |
|||
{ |
|||
if (isGuid) |
|||
{ |
|||
// Special length indicator for all guids.
|
|||
buffer[offset++] = 0xFF; |
|||
WriteGuid(buffer.AsSpan(offset), guid); |
|||
|
|||
return offset + GuidLength; |
|||
} |
|||
else |
|||
{ |
|||
// We assume that we use relatively small IDs, not longer than 254 bytes.
|
|||
buffer[offset++] = (byte)idLength; |
|||
WriteString(buffer.AsSpan(offset), id); |
|||
|
|||
return offset + idLength; |
|||
} |
|||
} |
|||
|
|||
context.Writer.WriteBytes(bufferArray); |
|||
} |
|||
|
|||
private static (int Length, bool IsGuid, Guid Guid, string Source) CheckId(DomainId id) |
|||
{ |
|||
var source = id.ToString(); |
|||
|
|||
var idIsGuid = Guid.TryParse(source, out var idGuid); |
|||
var idLength = GuidLength; |
|||
|
|||
if (!idIsGuid) |
|||
{ |
|||
idLength = (byte)Encoding.UTF8.GetByteCount(source); |
|||
|
|||
// We only use a single byte to write the length, therefore we do not allow large strings.
|
|||
if (idLength > 254) |
|||
{ |
|||
ThrowHelper.InvalidOperationException("Cannot write long IDs."); |
|||
} |
|||
} |
|||
|
|||
return (idLength, idIsGuid, idGuid, source); |
|||
} |
|||
|
|||
private static void WriteString(Span<byte> span, string id) |
|||
{ |
|||
Encoding.UTF8.GetBytes(id, span); |
|||
} |
|||
|
|||
private static void WriteGuid(Span<byte> span, Guid guid) |
|||
{ |
|||
guid.TryWriteBytes(span); |
|||
} |
|||
} |
|||
@ -0,0 +1,98 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Runtime.CompilerServices; |
|||
using NodaTime; |
|||
using Squidex.Domain.Apps.Core.Apps; |
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Domain.Apps.Core.Schemas; |
|||
using Squidex.Domain.Apps.Entities.Contents; |
|||
using Squidex.Domain.Apps.Entities.Contents.Repositories; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Queries; |
|||
using Squidex.Infrastructure.States; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents; |
|||
|
|||
public sealed class MongoShardedContentRepository : ShardedSnapshotStore<MongoContentRepository, WriteContent>, IContentRepository |
|||
{ |
|||
public MongoShardedContentRepository(IShardingStrategy sharding, Func<string, MongoContentRepository> factory) |
|||
: base(sharding, factory) |
|||
{ |
|||
} |
|||
|
|||
public Task<Content?> FindContentAsync(App app, Schema schema, DomainId id, SearchScope scope, |
|||
CancellationToken ct = default) |
|||
{ |
|||
return Shard(app.Id).FindContentAsync(app, schema, id, scope, ct); |
|||
} |
|||
|
|||
public Task<bool> HasReferrersAsync(App app, DomainId reference, SearchScope scope, |
|||
CancellationToken ct = default) |
|||
{ |
|||
return Shard(app.Id).HasReferrersAsync(app, reference, scope, ct); |
|||
} |
|||
|
|||
public Task<IResultList<Content>> QueryAsync(App app, List<Schema> schemas, Q q, SearchScope scope, |
|||
CancellationToken ct = default) |
|||
{ |
|||
return Shard(app.Id).QueryAsync(app, schemas, q, scope, ct); |
|||
} |
|||
|
|||
public Task<IResultList<Content>> QueryAsync(App app, Schema schema, Q q, SearchScope scope, |
|||
CancellationToken ct = default) |
|||
{ |
|||
return Shard(app.Id).QueryAsync(app, schema, q, scope, ct); |
|||
} |
|||
|
|||
public Task<IReadOnlyList<ContentIdStatus>> QueryIdsAsync(App app, Schema schema, FilterNode<ClrValue> filterNode, SearchScope scope, |
|||
CancellationToken ct = default) |
|||
{ |
|||
return Shard(app.Id).QueryIdsAsync(app, schema, filterNode, scope, ct); |
|||
} |
|||
|
|||
public Task<IReadOnlyList<ContentIdStatus>> QueryIdsAsync(App app, HashSet<DomainId> ids, SearchScope scope, |
|||
CancellationToken ct = default) |
|||
{ |
|||
return Shard(app.Id).QueryIdsAsync(app, ids, scope, ct); |
|||
} |
|||
|
|||
public Task ResetScheduledAsync(DomainId appId, DomainId id, SearchScope scope, |
|||
CancellationToken ct = default) |
|||
{ |
|||
return Shard(appId).ResetScheduledAsync(appId, id, scope, ct); |
|||
} |
|||
|
|||
public IAsyncEnumerable<Content> StreamAll(DomainId appId, HashSet<DomainId>? schemaIds, SearchScope scope, |
|||
CancellationToken ct = default) |
|||
{ |
|||
return Shard(appId).StreamAll(appId, schemaIds, scope, ct); |
|||
} |
|||
|
|||
public IAsyncEnumerable<Content> StreamReferencing(DomainId appId, DomainId references, int take, SearchScope scope, |
|||
CancellationToken ct = default) |
|||
{ |
|||
return Shard(appId).StreamReferencing(appId, references, take, scope, ct); |
|||
} |
|||
|
|||
public async IAsyncEnumerable<Content> StreamScheduledWithoutDataAsync(Instant now, SearchScope scope, |
|||
[EnumeratorCancellation] CancellationToken ct = default) |
|||
{ |
|||
foreach (var shard in Shards) |
|||
{ |
|||
await foreach (var content in shard.StreamScheduledWithoutDataAsync(now, scope, ct)) |
|||
{ |
|||
yield return content; |
|||
} |
|||
} |
|||
} |
|||
|
|||
protected override string GetShardKey(WriteContent state) |
|||
{ |
|||
return GetShardKey(state.AppId.Id); |
|||
} |
|||
} |
|||
@ -0,0 +1,101 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Runtime.CompilerServices; |
|||
using Squidex.Domain.Apps.Core.Apps; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.States; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.MongoDb; |
|||
|
|||
public abstract class ShardedSnapshotStore<T, TState> : ShardedService<T>, ISnapshotStore<TState>, IDeleter where T : ISnapshotStore<TState>, IDeleter |
|||
{ |
|||
protected ShardedSnapshotStore(IShardingStrategy sharding, Func<string, T> factory) |
|||
: base(sharding, factory) |
|||
{ |
|||
} |
|||
|
|||
protected abstract string GetShardKey(TState state); |
|||
|
|||
public Task WriteAsync(SnapshotWriteJob<TState> job, |
|||
CancellationToken ct = default) |
|||
{ |
|||
var shard = Shard(GetShardKey(job.Value)); |
|||
|
|||
return shard.WriteAsync(job, ct); |
|||
} |
|||
|
|||
public Task<SnapshotResult<TState>> ReadAsync(DomainId key, |
|||
CancellationToken ct = default) |
|||
{ |
|||
var shard = Shard(GetAppId(key)); |
|||
|
|||
return shard.ReadAsync(key, ct); |
|||
} |
|||
|
|||
public Task RemoveAsync(DomainId key, |
|||
CancellationToken ct = default) |
|||
{ |
|||
var shard = Shard(GetAppId(key)); |
|||
|
|||
return shard.RemoveAsync(key, ct); |
|||
} |
|||
|
|||
public Task DeleteAppAsync(App app, |
|||
CancellationToken ct) |
|||
{ |
|||
var shard = Shard(app.Id); |
|||
|
|||
return shard.DeleteAppAsync(app, ct); |
|||
} |
|||
|
|||
public async IAsyncEnumerable<SnapshotResult<TState>> ReadAllAsync( |
|||
[EnumeratorCancellation] CancellationToken ct = default) |
|||
{ |
|||
foreach (var shard in Shards) |
|||
{ |
|||
await foreach (var asset in shard.ReadAllAsync(ct)) |
|||
{ |
|||
yield return asset; |
|||
} |
|||
} |
|||
} |
|||
|
|||
public async Task ClearAsync( |
|||
CancellationToken ct = default) |
|||
{ |
|||
foreach (var shard in Shards) |
|||
{ |
|||
await shard.ClearAsync(ct); |
|||
} |
|||
} |
|||
|
|||
public async Task WriteManyAsync(IEnumerable<SnapshotWriteJob<TState>> jobs, |
|||
CancellationToken ct = default) |
|||
{ |
|||
// Some commands might share a shared, therefore we don't group by app id.
|
|||
foreach (var byShard in jobs.GroupBy(c => GetShardKey(c.Value))) |
|||
{ |
|||
var shard = Shard(byShard.Key); |
|||
|
|||
await shard.WriteManyAsync(byShard.ToArray(), ct); |
|||
} |
|||
} |
|||
|
|||
private static DomainId GetAppId(DomainId key) |
|||
{ |
|||
// This is a leaky abstraction, but the only option to implement that in a fast way.
|
|||
var parts = key.ToString().Split(DomainId.IdSeparator); |
|||
|
|||
if (parts.Length != 2) |
|||
{ |
|||
throw new InvalidOperationException("The key does not contain an app id."); |
|||
} |
|||
|
|||
return DomainId.Create(parts[0]); |
|||
} |
|||
} |
|||
@ -0,0 +1,55 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Domain.Apps.Core.Apps; |
|||
using Squidex.Domain.Apps.Entities.Contents; |
|||
using Squidex.Domain.Apps.Entities.Contents.Text; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.States; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.MongoDb.Text; |
|||
|
|||
public sealed class MongoShardedTextIndex<T> : ShardedService<MongoTextIndexBase<T>>, ITextIndex where T : class |
|||
{ |
|||
public MongoShardedTextIndex(IShardingStrategy sharding, Func<string, MongoTextIndexBase<T>> factory) |
|||
: base(sharding, factory) |
|||
{ |
|||
} |
|||
|
|||
public async Task ClearAsync( |
|||
CancellationToken ct = default) |
|||
{ |
|||
foreach (var shard in Shards) |
|||
{ |
|||
await shard.ClearAsync(ct); |
|||
} |
|||
} |
|||
|
|||
public async Task ExecuteAsync(IndexCommand[] commands, |
|||
CancellationToken ct = default) |
|||
{ |
|||
// Some commands might share a shared, therefore we don't group by app id.
|
|||
foreach (var byShard in commands.GroupBy(c => GetShardKey(c.UniqueContentId.AppId))) |
|||
{ |
|||
var shard = Shard(byShard.Key); |
|||
|
|||
await shard.ExecuteAsync(byShard.ToArray(), ct); |
|||
} |
|||
} |
|||
|
|||
public Task<List<DomainId>?> SearchAsync(App app, TextQuery query, SearchScope scope, |
|||
CancellationToken ct = default) |
|||
{ |
|||
return Shard(app.Id).SearchAsync(app, query, scope, ct); |
|||
} |
|||
|
|||
public Task<List<DomainId>?> SearchAsync(App app, GeoQuery query, SearchScope scope, |
|||
CancellationToken ct = default) |
|||
{ |
|||
return Shard(app.Id).SearchAsync(app, query, scope, ct); |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.Text.State; |
|||
|
|||
public enum TextState |
|||
{ |
|||
Stage0_Draft__Stage1_None, |
|||
Stage0_Published__Stage1_None, |
|||
Stage0_Published__Stage1_Draft, |
|||
Stage1_Draft__Stage0_None, |
|||
Stage1_Published__Stage0_None, |
|||
Stage1_Published__Stage0_Draft, |
|||
Deleted, |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure; |
|||
|
|||
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
|
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.Text; |
|||
|
|||
public record struct UniqueContentId(DomainId AppId, DomainId ContentId) |
|||
{ |
|||
} |
|||
@ -0,0 +1,68 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Hosting; |
|||
|
|||
namespace Squidex.Infrastructure.States; |
|||
|
|||
public abstract class ShardedService<T> : IInitializable |
|||
{ |
|||
private readonly Dictionary<string, T> shards = new Dictionary<string, T>(); |
|||
private readonly IShardingStrategy sharding; |
|||
private readonly Func<string, T> factory; |
|||
|
|||
protected IEnumerable<T> Shards => shards.Values; |
|||
|
|||
protected ShardedService(IShardingStrategy sharding, Func<string, T> factory) |
|||
{ |
|||
this.sharding = sharding; |
|||
this.factory = factory; |
|||
} |
|||
|
|||
public async Task InitializeAsync( |
|||
CancellationToken ct) |
|||
{ |
|||
foreach (var shardKey in sharding.GetShardKeys()) |
|||
{ |
|||
var inner = factory(shardKey); |
|||
|
|||
if (inner is IInitializable initializable) |
|||
{ |
|||
await initializable.InitializeAsync(ct); |
|||
} |
|||
|
|||
shards[shardKey] = inner; |
|||
} |
|||
} |
|||
|
|||
public async Task ReleaseAsync( |
|||
CancellationToken ct) |
|||
{ |
|||
foreach (var inner in shards.Values) |
|||
{ |
|||
if (inner is IInitializable initializable) |
|||
{ |
|||
await initializable.ReleaseAsync(ct); |
|||
} |
|||
} |
|||
} |
|||
|
|||
protected string GetShardKey<TKey>(TKey key) where TKey : notnull |
|||
{ |
|||
return sharding.GetShardKey(key); |
|||
} |
|||
|
|||
protected T Shard<TKey>(TKey key) where TKey : notnull |
|||
{ |
|||
return shards[GetShardKey(key)]; |
|||
} |
|||
|
|||
protected string GetShardKey(DomainId appId) |
|||
{ |
|||
return sharding.GetShardKey(appId); |
|||
} |
|||
} |
|||
@ -0,0 +1,66 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
#pragma warning disable MA0048 // File name must match type name
|
|||
|
|||
namespace Squidex.Infrastructure.States; |
|||
|
|||
public interface IShardingStrategy |
|||
{ |
|||
string GetShardKey<T>(T key) where T : notnull; |
|||
|
|||
IEnumerable<string> GetShardKeys(); |
|||
} |
|||
|
|||
public sealed class SingleSharding : IShardingStrategy |
|||
{ |
|||
public static readonly IShardingStrategy Instance = new SingleSharding(); |
|||
|
|||
private SingleSharding() |
|||
{ |
|||
} |
|||
|
|||
public string GetShardKey<T>(T key) where T : notnull |
|||
{ |
|||
return string.Empty; |
|||
} |
|||
|
|||
public IEnumerable<string> GetShardKeys() |
|||
{ |
|||
yield return string.Empty; |
|||
} |
|||
} |
|||
|
|||
public sealed class PartitionedSharding : IShardingStrategy |
|||
{ |
|||
private readonly int numPartitions; |
|||
|
|||
public PartitionedSharding(int numPartitions) |
|||
{ |
|||
this.numPartitions = numPartitions; |
|||
} |
|||
|
|||
public string GetShardKey<T>(T key) where T : notnull |
|||
{ |
|||
var partition = Math.Abs(key.GetHashCode()) % numPartitions; |
|||
|
|||
return GetShardKey(partition); |
|||
} |
|||
|
|||
public IEnumerable<string> GetShardKeys() |
|||
{ |
|||
for (var i = 0; i < numPartitions; i++) |
|||
{ |
|||
yield return GetShardKey(i); |
|||
} |
|||
} |
|||
|
|||
private static string GetShardKey(int partition) |
|||
{ |
|||
return $"_{partition}"; |
|||
} |
|||
} |
|||
@ -0,0 +1,90 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Domain.Apps.Core.TestHelpers; |
|||
using Squidex.Domain.Apps.Entities.MongoDb; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.Text; |
|||
|
|||
public class BsonUniqueContentIdSerializerTests |
|||
{ |
|||
public BsonUniqueContentIdSerializerTests() |
|||
{ |
|||
BsonUniqueContentIdSerializer.Register(); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_serialize_and_deserialize_guid_guid() |
|||
{ |
|||
var source = new UniqueContentId(DomainId.NewGuid(), DomainId.NewGuid()); |
|||
|
|||
var deserialized = source.SerializeAndDeserializeBson(); |
|||
|
|||
Assert.Equal(source, deserialized); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_serialize_and_deserialize_guid_custom() |
|||
{ |
|||
var source = new UniqueContentId(DomainId.NewGuid(), DomainId.Create("id42")); |
|||
|
|||
var deserialized = source.SerializeAndDeserializeBson(); |
|||
|
|||
Assert.Equal(source, deserialized); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_serialize_and_deserialize_guid_empty() |
|||
{ |
|||
var source = new UniqueContentId(DomainId.NewGuid(), DomainId.Empty); |
|||
|
|||
var deserialized = source.SerializeAndDeserializeBson(); |
|||
|
|||
Assert.Equal(source, deserialized); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_serialize_and_deserialize_custom_custom() |
|||
{ |
|||
var source = new UniqueContentId(DomainId.Create("id41"), DomainId.Create("id42")); |
|||
|
|||
var deserialized = source.SerializeAndDeserializeBson(); |
|||
|
|||
Assert.Equal(source, deserialized); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_serialize_and_deserialize_custom_guid() |
|||
{ |
|||
var source = new UniqueContentId(DomainId.Create("id42"), DomainId.NewGuid()); |
|||
|
|||
var deserialized = source.SerializeAndDeserializeBson(); |
|||
|
|||
Assert.Equal(source, deserialized); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_calculate_next_custom_id() |
|||
{ |
|||
var appId = DomainId.Create("x"); |
|||
|
|||
var actual = BsonUniqueContentIdSerializer.NextAppId(appId); |
|||
|
|||
Assert.Equal(new UniqueContentId(DomainId.Create("y"), DomainId.Empty), actual); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_calculate_next_guid_id() |
|||
{ |
|||
var appId = DomainId.Create("70fb3772-2ab5-4854-b421-054d2479a0f7"); |
|||
|
|||
var actual = BsonUniqueContentIdSerializer.NextAppId(appId); |
|||
|
|||
Assert.Equal(new UniqueContentId(DomainId.Create("70fb3773-2ab5-4854-b421-054d2479a0f7"), DomainId.Empty), actual); |
|||
} |
|||
} |
|||
@ -0,0 +1,38 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Domain.Apps.Core.TestHelpers; |
|||
using Squidex.Domain.Apps.Entities.MongoDb.Text; |
|||
using Squidex.Domain.Apps.Entities.TestHelpers; |
|||
using Squidex.Infrastructure.MongoDb; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.Text; |
|||
|
|||
public sealed class MongoTextIndexerStateFixture : IAsyncLifetime |
|||
{ |
|||
public MongoTextIndexerState State { get; } |
|||
|
|||
public MongoTextIndexerStateFixture() |
|||
{ |
|||
TestUtils.SetupBson(); |
|||
|
|||
var mongoClient = MongoClientFactory.Create(TestConfig.Configuration["mongoDb:configuration"]!); |
|||
var mongoDatabase = mongoClient.GetDatabase(TestConfig.Configuration["mongodb:database"]!); |
|||
|
|||
State = new MongoTextIndexerState(mongoDatabase); |
|||
} |
|||
|
|||
public Task InitializeAsync() |
|||
{ |
|||
return State.InitializeAsync(default); |
|||
} |
|||
|
|||
public Task DisposeAsync() |
|||
{ |
|||
return Task.CompletedTask; |
|||
} |
|||
} |
|||
@ -0,0 +1,95 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
#pragma warning disable SA1300 // Element should begin with upper-case letter
|
|||
|
|||
using Squidex.Domain.Apps.Core.Apps; |
|||
using Squidex.Domain.Apps.Entities.Contents.Text.State; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.Text; |
|||
|
|||
[Trait("Category", "Dependencies")] |
|||
public class MongoTextIndexerStateTests : IClassFixture<MongoTextIndexerStateFixture> |
|||
{ |
|||
public MongoTextIndexerStateFixture _ { get; set; } |
|||
|
|||
public MongoTextIndexerStateTests(MongoTextIndexerStateFixture fixture) |
|||
{ |
|||
_ = fixture; |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_add_state() |
|||
{ |
|||
var appId = DomainId.NewGuid(); |
|||
var id1 = new UniqueContentId(appId, DomainId.NewGuid()); |
|||
var id2 = new UniqueContentId(appId, DomainId.NewGuid()); |
|||
var id3 = new UniqueContentId(appId, DomainId.NewGuid()); |
|||
|
|||
await _.State.SetAsync( |
|||
[ |
|||
new TextContentState { UniqueContentId = id1, State = TextState.Stage0_Draft__Stage1_None }, |
|||
new TextContentState { UniqueContentId = id2, State = TextState.Stage0_Published__Stage1_Draft }, |
|||
new TextContentState { UniqueContentId = id3, State = TextState.Stage0_Published__Stage1_None } |
|||
]); |
|||
|
|||
var actual = await _.State.GetAsync(HashSet.Of(id1, id2)); |
|||
|
|||
actual.Should().BeEquivalentTo(new Dictionary<UniqueContentId, TextContentState> |
|||
{ |
|||
[id1] = new TextContentState { UniqueContentId = id1, State = TextState.Stage0_Draft__Stage1_None }, |
|||
[id2] = new TextContentState { UniqueContentId = id2, State = TextState.Stage0_Published__Stage1_Draft } |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_remove_state() |
|||
{ |
|||
var id = new UniqueContentId(DomainId.NewGuid(), DomainId.NewGuid()); |
|||
|
|||
await _.State.SetAsync( |
|||
[ |
|||
new TextContentState { UniqueContentId = id, State = TextState.Stage0_Draft__Stage1_None } |
|||
]); |
|||
|
|||
await _.State.SetAsync( |
|||
[ |
|||
new TextContentState { UniqueContentId = id, State = TextState.Deleted } |
|||
]); |
|||
|
|||
var actual = await _.State.GetAsync(HashSet.Of(id)); |
|||
|
|||
Assert.Empty(actual); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_remove_by_app_state() |
|||
{ |
|||
var appId1 = DomainId.NewGuid(); |
|||
var appId2 = DomainId.NewGuid(); |
|||
var id1 = new UniqueContentId(appId1, DomainId.NewGuid()); |
|||
var id2 = new UniqueContentId(appId1, DomainId.NewGuid()); |
|||
var id3 = new UniqueContentId(appId2, DomainId.NewGuid()); |
|||
|
|||
await _.State.SetAsync( |
|||
[ |
|||
new TextContentState { UniqueContentId = id1, State = TextState.Stage0_Draft__Stage1_None }, |
|||
new TextContentState { UniqueContentId = id2, State = TextState.Stage0_Published__Stage1_Draft }, |
|||
new TextContentState { UniqueContentId = id3, State = TextState.Stage0_Published__Stage1_None } |
|||
]); |
|||
|
|||
await ((IDeleter)_.State).DeleteAppAsync(new App { Id = appId1 }, default); |
|||
|
|||
var actual = await _.State.GetAsync(HashSet.Of(id1, id2, id3)); |
|||
|
|||
actual.Should().BeEquivalentTo(new Dictionary<UniqueContentId, TextContentState> |
|||
{ |
|||
[id3] = new TextContentState { UniqueContentId = id3, State = TextState.Stage0_Published__Stage1_None } |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,82 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Hosting; |
|||
|
|||
namespace Squidex.Infrastructure.States; |
|||
|
|||
public class ShardedServiceTests |
|||
{ |
|||
private readonly IInner inner1 = A.Fake<IInner>(); |
|||
private readonly IInner inner2 = A.Fake<IInner>(); |
|||
private readonly TestSut sut; |
|||
|
|||
public interface IInner : IInitializable |
|||
{ |
|||
} |
|||
|
|||
private class TestSut : ShardedService<IInner> |
|||
{ |
|||
public TestSut(IShardingStrategy sharding, Func<string, IInner> factory) |
|||
: base(sharding, factory) |
|||
{ |
|||
} |
|||
|
|||
public IInner ExposeShard<TKey>(TKey key) where TKey : notnull |
|||
{ |
|||
return Shard(key); |
|||
} |
|||
} |
|||
|
|||
public ShardedServiceTests() |
|||
{ |
|||
sut = new TestSut(new PartitionedSharding(2), key => |
|||
{ |
|||
if (key == "_0") |
|||
{ |
|||
return inner1; |
|||
} |
|||
else |
|||
{ |
|||
return inner2; |
|||
} |
|||
}); |
|||
|
|||
sut.InitializeAsync(default).Wait(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_initialize_shards() |
|||
{ |
|||
await sut.InitializeAsync(default); |
|||
|
|||
A.CallTo(() => inner1.InitializeAsync(A<CancellationToken>._)) |
|||
.MustHaveHappened(); |
|||
|
|||
A.CallTo(() => inner2.InitializeAsync(A<CancellationToken>._)) |
|||
.MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_release_shards() |
|||
{ |
|||
await sut.ReleaseAsync(default); |
|||
|
|||
A.CallTo(() => inner1.ReleaseAsync(A<CancellationToken>._)) |
|||
.MustHaveHappened(); |
|||
|
|||
A.CallTo(() => inner2.ReleaseAsync(A<CancellationToken>._)) |
|||
.MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_provide_shards() |
|||
{ |
|||
Assert.Equal(inner1, sut.ExposeShard(0)); |
|||
Assert.Equal(inner2, sut.ExposeShard(1)); |
|||
} |
|||
} |
|||
@ -0,0 +1,57 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Infrastructure.States; |
|||
|
|||
public class ShardingTests |
|||
{ |
|||
[Fact] |
|||
public void Should_provide_single_shard_key() |
|||
{ |
|||
var strategy = SingleSharding.Instance; |
|||
|
|||
for (var i = 0; i < 1000; i++) |
|||
{ |
|||
var shardKey = strategy.GetShardKey(Guid.NewGuid()); |
|||
|
|||
Assert.Equal(string.Empty, shardKey); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_provide_single_shard_keys() |
|||
{ |
|||
var strategy = SingleSharding.Instance; |
|||
|
|||
var shardKeys = strategy.GetShardKeys().ToArray(); |
|||
|
|||
Assert.Equal(new[] { string.Empty }, shardKeys); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_provide_partitioned_shard_key() |
|||
{ |
|||
var strategy = new PartitionedSharding(3); |
|||
|
|||
for (var i = 0; i < 1000; i++) |
|||
{ |
|||
var shardKey = strategy.GetShardKey(Guid.NewGuid()); |
|||
|
|||
Assert.True(shardKey is "_0" or "_1" or "_2"); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_provide_partitioned_shard_keys() |
|||
{ |
|||
var strategy = new PartitionedSharding(3); |
|||
|
|||
var shardKeys = strategy.GetShardKeys().ToArray(); |
|||
|
|||
Assert.Equal(new[] { "_0", "_1", "_2" }, shardKeys); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue