/// <summary> /// Track SignalDispatches that are responsible for consolidation. /// Required even if single Sender instance is running and no lock is set in database. /// Tracking pending consolidations allows to choose only single consolidation root that will attach other dispatches to itself. /// </summary> /// <param name="signal"></param> /// <returns></returns> public virtual ConsolidationLock <TKey> GetOrAddLock(SignalDispatch <TKey> signal) { //Lock for signal group and this signal as ConsolidationRoot ConsolidationLock <TKey> groupLock = ToConsolidationLock(signal); bool isNewLock = _locksCache.AddIfUnique(groupLock); //Lock with same group key can be added in parallel //First added lock will be set ConsolidationRootId. groupLock = _locksCache.GetMatchingGroup(groupLock); bool isConsolidationRoot = EqualityComparer <TKey> .Default.Equals( groupLock.ConsolidationRootId, signal.SignalDispatchId); if (!isConsolidationRoot) { //Signal will not start consolidation, but will be attached to ConsolidationRoot return(groupLock); } if (!_settings.IsDbLockStorageEnabled) { return(groupLock); } if (isNewLock) { bool isDuplicate = _consolidationLockQueries.InsertOneHandleDuplicate(groupLock).Result; if (isDuplicate) { //get latest value with same group key to cache it //db can return null if some other process will clear ConsolidationLocks collection in the middle groupLock = _consolidationLockQueries.FindExistingMatch(groupLock).Result; _locksCache.ReplaceGroup(groupLock); } } if (groupLock == null) { return(null); } bool isLockedByCurrentSenderInstance = groupLock.LockedBy == _settings.LockedByInstanceId; bool isLockExpired = CheckIsExpired(groupLock, expireBeforehand: isLockedByCurrentSenderInstance); if (isLockExpired) { groupLock.LockedBy = _settings.LockedByInstanceId.Value; bool lockExtended = _consolidationLockQueries.ExtendLockTime(groupLock).Result; if (!lockExtended) { //get latest value with same group key to store it in cache //db can return null if some other process will clear ConsolidationLocks collection in the middle groupLock = _consolidationLockQueries.FindExistingMatch(groupLock).Result; } //update LockedSinceUtc or LockedSinceUtc with LockedBy _locksCache.ReplaceGroup(groupLock); } return(groupLock); }
//find methods public virtual Task <ConsolidationLock <ObjectId> > FindExistingMatch(ConsolidationLock <ObjectId> consolidationLock, CancellationToken token = default) { Expression <Func <ConsolidationLock <ObjectId>, bool> > filter = x => x.ReceiverSubscriberId == consolidationLock.ReceiverSubscriberId && x.CategoryId == consolidationLock.CategoryId && x.DeliveryType == consolidationLock.DeliveryType; return(FindOne(filter, token)); }
public virtual ConsolidationLock <TKey> GetMatchingGroup(ConsolidationLock <TKey> item) { _itemsLock.EnterReadLock(); try { return(_items.FirstOrDefault(x => x == item)); } finally { _itemsLock.ExitReadLock(); } }
//methods public bool Execute(SignalWrapper <SignalDispatch <TKey> > item) { ConsolidationLock <TKey> groupLock = _consolidationLockTracker.GetOrAddLock(item.Signal); if (groupLock == null) { //other process cleared consolidation locks table in the middle //just repeat processing later _dispatchQueue.ApplyResult(item, ProcessingResult.Repeat); return(false); } bool isLockedByCurrentInstance = groupLock.LockedBy == _settings.LockedByInstanceId; if (!isLockedByCurrentInstance) { //this consolidation is handled by other Sender instance. //databaes Signal will be removed after consolidation finished but responsible Sender instance. return(false); } bool isConsolidationRoot = EqualityComparer <TKey> .Default.Equals( groupLock.ConsolidationRootId, item.Signal.SignalDispatchId); if (isConsolidationRoot) { //lock was created and this dispatch is now consolidation root. //can start consolidation. return(true); } bool willBeConsolidatedWithCurrentBatch = item.Signal.CreateDateUtc < groupLock.ConsolidationRootSendDateUtc; if (willBeConsolidatedWithCurrentBatch) { //this dispatch will be atteched to ConsolidationRoot. //databaes Signal will be removed after consolidation finished. return(false); } //next consolidation batch should start after current batch is finished. //because need to prevent selecting extra items from previous batch and attach them twice to different consolidation batches. //repeat processing item later. _dispatchQueue.ApplyResult(item, ProcessingResult.Repeat); return(false); }
//update methods public virtual async Task <bool> ExtendLockTime(ConsolidationLock <ObjectId> lockToExtend, CancellationToken token = default) { Expression <Func <ConsolidationLock <ObjectId>, bool> > filter = x => x.ReceiverSubscriberId == lockToExtend.ReceiverSubscriberId && x.CategoryId == lockToExtend.CategoryId && x.DeliveryType == lockToExtend.DeliveryType && x.LockedSinceUtc == lockToExtend.LockedSinceUtc; //make sure it was not updated by some other Sender instance already var updates = Updates <ConsolidationLock <ObjectId> > .Empty() .Set(x => x.LockedSinceUtc, DateTime.UtcNow) .Set(x => x.LockedBy, lockToExtend.LockedBy); long docsUpdates = await UpdateOne(filter, updates, token).ConfigureAwait(false); bool lockExtended = docsUpdates > 0; return(lockExtended); }
//methods public bool AddIfUnique(ConsolidationLock <TKey> item) { _itemsLock.EnterWriteLock(); bool added = false; try { ConsolidationLock <TKey> sameGroupLock = _items.FirstOrDefault(x => x == item); if (sameGroupLock == null) { added = true; _items.Add(item); } } finally { _itemsLock.ExitWriteLock(); } return(added); }
protected virtual bool CheckIsExpired(ConsolidationLock <TKey> consolidationLock, bool expireBeforehand) { bool isLockingEnabled = _settings.IsDbLockStorageEnabled; if (!isLockingEnabled) { //if only single Sender instance and database locking is disabled, then can not expire. return(false); } DateTime lockExpirationTime = consolidationLock.LockedSinceUtc.Value .Add(_settings.LockDuration); if (expireBeforehand) { //if lock is close to expiration, update it's value in database, so it is not expired during consolidation. //but do not treat as expired locks set by other Sender instances. lockExpirationTime = lockExpirationTime.Subtract(_expireBeforehandInterval); } return(lockExpirationTime < DateTime.UtcNow); }
public void WhenEqualSignIsUsedForNotMatchingLocks_ReturnsFalse() { //prepare var item1 = new ConsolidationLock <long> { CategoryId = 1, DeliveryType = 1, ReceiverSubscriberId = 1 }; var item2 = new ConsolidationLock <long> { CategoryId = 1, DeliveryType = 1, ReceiverSubscriberId = 2 }; //act bool areEqual = item1 == item2; //assert areEqual.ShouldBeFalse(); }
public void ReplaceGroup(ConsolidationLock <TKey> item) { if (item == null) { return; } _itemsLock.EnterWriteLock(); try { ConsolidationLock <TKey> sameGroupLock = _items.FirstOrDefault(x => x == item); if (sameGroupLock != null) { _items.Remove(sameGroupLock); } _items.Add(item); } finally { _itemsLock.ExitWriteLock(); } }
public Task <bool> InsertOneHandleDuplicate(ConsolidationLock <long> entity, CancellationToken token = default) { throw new NotImplementedException(); }
public Task <ConsolidationLock <long> > FindExistingMatch(ConsolidationLock <long> consolidationLock, CancellationToken token = default) { throw new NotImplementedException(); }
public Task <bool> ExtendLockTime(ConsolidationLock <long> lockToExtend, CancellationToken token = default) { throw new NotImplementedException(); }