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()); }
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); }