private void DoProcessQueue()
        {
            // begin read model transaction scope
            // get events for workitem.AggregateId
            // pass events to read model
            // commit

            // we might see a work item for an aggregate twice in a row
            // especially if we poke the queue when we receive events on
            // the event bus
            // in fact we can subscibe to events on the event bus and do our own poking

            var doMore = true;

            while (doMore)
            {
                var scope = this.scopeFactory();
                using (scope)
                {
                    var queue     = this.queueFactory(scope);
                    var workItems = queue.Peek(this.maxItems);
                    var bus       = new UnitOfWorkEventBus(this.context.EventBus);
                    scope.Add(bus);

                    foreach (var workItem in workItems)
                    {
                        if (this.cancel.IsCancellationRequested)
                        {
                            return;
                        }

                        var registration = this.registrations.FirstOrDefault(x => AggregateRootBase.GetAggregateTypeDescriptor(x.AggregateType) == workItem.AggregateType);
                        if (registration != null)
                        {
                            try
                            {
                                this.ProcessWorkItem(scope, registration, bus, workItem);
                                queue.Dequeue(workItem);
                            }
                            catch (Exception ex)
                            {
                                // TODO: handle sqlite busy exceptions
                                var topic = new DomainTopic(registration.AggregateType, workItem.Identity);
                                this.context.EventBus.Publish(new DomainNotification(topic, new ReadModelBuilderFaultedEvent(workItem.Identity, ex)));
                                throw;
                            }
                        }
                    }

                    scope.Commit();
                    doMore = workItems.Count == this.maxItems;
                }
            }
        }
        public void Execute(IList <IAggregateCommand> commands, int expectedVersion)
        {
            if (commands.Count == 0)
            {
                return;
            }

            // readModelBuilderBus will publish events to a bus that will build read models and then pass events off to
            // the real eventBus
            var readModelBuilderBus = new ReadModelBuildingEventBus(this.registration.ImmediateReadModels(this.scope), this.eventBus);

            // holds the events that were raised from the aggregate and will push them to the read model building bus
            var aggregateEvents = new UnitOfWorkEventBus(readModelBuilderBus);

            // subscribe to the changes in the aggregate and publish them to aggregateEvents
            var aggregateRepo = new AggregateRepository(registration.New, this.scope.GetRegisteredObject <IEventStore>(), scope.GetRegisteredObject <ISnapshotRepository>());
            var subscription  = aggregateRepo.Changes.Subscribe(aggregateEvents.Publish);

            this.scope.Add(new UnitOfWorkDisposable(subscription));

            // add them in this order so that aggregateEvents >> readModelBuilderBus >> read model builder >> eventBus
            this.scope.Add(aggregateEvents);
            this.scope.Add(readModelBuilderBus);

            var cmd = new CommandExecutor(aggregateRepo);

            cmd.Execute(commands.ToList(), expectedVersion);

            // enqueue pending commands
            this.EnqueueCommands(this.scope.GetRegisteredObject <IPendingCommandRepository>(), commands);

            // enqueue read models to be built - for non-immediate read models
            var typeName = AggregateRootBase.GetAggregateTypeDescriptor(registration.AggregateType);

            this.EnqueueReadModels(this.scope.GetRegisteredObject <IReadModelQueueProducer>(), typeName, aggregateEvents.GetEvents().Select(x => x.Event).OfType <IAggregateEvent>().ToList());
        }
예제 #3
0
        public bool SyncWithRemote <T>(Guid aggregateId) where T : class, IAggregateRoot, new()
        {
            var currentVersion = this.localEventStore.GetCurrentVersion(aggregateId);

            var syncState = this.syncStateRepository.GetById(aggregateId);

            // if no sync state, then assume that the aggregate originated from here, or other repo and that this is the first sync
            if (syncState == null)
            {
                syncState               = this.syncStateRepository.New();
                syncState.Identity      = aggregateId;
                syncState.AggregateType = AggregateRootBase.GetAggregateTypeDescriptor <T>();
            }

            var commonEvents        = this.localEventStore.GetEventsUpToVersion(aggregateId, syncState.LastSyncedVersion);
            var newRemoteEvents     = this.remoteEventStore.GetEventsAfterVersion(aggregateId, syncState.LastSyncedVersion);
            var unsyncedLocalEvents = this.localEventStore.GetEventsAfterVersion(aggregateId, syncState.LastSyncedVersion);
            var newCommonHistory    = commonEvents.Concat(newRemoteEvents).ToList();

            var currentRemoteVersion = newRemoteEvents.Count > 0 ? newRemoteEvents.Last().Version : syncState.LastSyncedVersion;

            if (newRemoteEvents.Count == 0 && unsyncedLocalEvents.Count == 0)
            {
                // ain't nothin' to merge (in either direction) - all done
                return(false);
            }

            // check for conflicts, anything we have done that conflicts with things that others have done
            var hasConflicts = unsyncedLocalEvents.Any(local => newRemoteEvents.Any(remote => local.ConflictsWith(remote)));

            if (hasConflicts)
            {
                throw new ConflictException("Pending commands conflict with new remote events");
            }

            // get the changes that we have that need to be sent to remote, either from pending commands
            // or because we've never synced at all
            var pendingEvents = this.GetPendingEvents <T>(syncState, newCommonHistory);

            this.pendingCommands.RemovePendingCommands(aggregateId);

            // we need to do a full rebuild if we have previously synced with remote and we merged in any remote events
            var needsFullRebuild = syncState.LastSyncedVersion > 0 && newRemoteEvents.Count != 0;

            // if we're receiving an aggregate with changes and we have no changes, this fails
            var newVersion = pendingEvents.Count > 0 ? pendingEvents.Last().Version : currentRemoteVersion;

            // merge remote events into our eventstore - including our pending events
            if (newRemoteEvents.Count != 0)
            {
                this.localEventStore.MergeEvents(aggregateId, newRemoteEvents.Concat(pendingEvents).ToList(), currentVersion, syncState.LastSyncedVersion);
            }

            // update sync state
            syncState.LastSyncedVersion = newVersion;
            this.syncStateRepository.Save(syncState);

            // update remote
            if (pendingEvents.Count != 0)
            {
                this.remoteEventStore.SaveEvents(aggregateId, pendingEvents, currentRemoteVersion);
            }

            return(needsFullRebuild);
        }