// ========================================================================== // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex UG (haftungsbeschraenkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using System.Threading.Tasks.Dataflow; using Microsoft.Extensions.DependencyInjection; using Squidex.Caching; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.States; #pragma warning disable RECS0108 // Warns about static fields in generic types namespace Squidex.Infrastructure.Commands { public delegate Task IdSource(Func add); public class Rebuilder { private readonly ILocalCache localCache; private readonly IEventStore eventStore; private readonly IServiceProvider serviceProvider; private static class Factory where T : DomainObject where TState : class, IDomainState, new() { private static readonly ObjectFactory ObjectFactory = ActivatorUtilities.CreateFactory(typeof(T), new[] { typeof(IPersistenceFactory) }); public static T Create(IServiceProvider serviceProvider, IPersistenceFactory persistenceFactory) { return (T)ObjectFactory(serviceProvider, new object[] { persistenceFactory }); } } public Rebuilder( ILocalCache localCache, IEventStore eventStore, IServiceProvider serviceProvider) { Guard.NotNull(localCache, nameof(localCache)); Guard.NotNull(serviceProvider, nameof(serviceProvider)); Guard.NotNull(eventStore, nameof(eventStore)); this.eventStore = eventStore; this.serviceProvider = serviceProvider; this.localCache = localCache; } public virtual async Task RebuildAsync(string filter, CancellationToken ct) where T : DomainObject where TState : class, IDomainState, new() { var store = serviceProvider.GetRequiredService>(); await store.ClearSnapshotsAsync(); await InsertManyAsync(store, async target => { await eventStore.QueryAsync(async storedEvent => { var id = storedEvent.Data.Headers.AggregateId(); await target(id); }, filter, ct: ct); }, ct); } public virtual async Task InsertManyAsync(IEnumerable source, CancellationToken ct = default) where T : DomainObject where TState : class, IDomainState, new() { Guard.NotNull(source, nameof(source)); var store = serviceProvider.GetRequiredService>(); await InsertManyAsync(store, async target => { foreach (var id in source) { await target(id); } }, ct); } private async Task InsertManyAsync(IStore store, IdSource source, CancellationToken ct = default) where T : DomainObject where TState : class, IDomainState, new() { var parallelism = Environment.ProcessorCount; const int BatchSize = 100; var workerBlock = new ActionBlock(async ids => { try { await using (var context = store.WithBatchContext(typeof(T))) { await context.LoadAsync(ids); foreach (var id in ids) { try { var domainObject = Factory.Create(serviceProvider, context); domainObject.Setup(id); await domainObject.RebuildStateAsync(); } catch (DomainObjectNotFoundException) { return; } } } } catch (OperationCanceledException ex) { // Dataflow swallows operation cancelled exception. throw new AggregateException(ex); } }, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = parallelism, MaxMessagesPerTask = 10, BoundedCapacity = parallelism }); var batchBlock = new BatchBlock(BatchSize, new GroupingDataflowBlockOptions { BoundedCapacity = BatchSize }); batchBlock.LinkTo(workerBlock, new DataflowLinkOptions { PropagateCompletion = true }); var handledIds = new HashSet(); using (localCache.StartContext()) { await source(id => { if (handledIds.Add(id)) { return batchBlock.SendAsync(id, ct); } return Task.CompletedTask; }); batchBlock.Complete(); await workerBlock.Completion; } } } }