// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // =+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ // // JoinBlock.cs // // // Blocks that join multiple messages of different types together into a tuple, // with one item per type. // // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Linq; using System.Security; using System.Threading.Tasks.Dataflow.Internal; namespace System.Threading.Tasks.Dataflow { /// /// Provides a dataflow block that joins across multiple dataflow sources, not necessarily of the same type, /// waiting for one item to arrive for each type before they’re all released together as a tuple of one item per type. /// /// Specifies the type of data accepted by the block's first target. /// Specifies the type of data accepted by the block's second target. [DebuggerDisplay("{DebuggerDisplayContent,nq}")] [DebuggerTypeProxy(typeof(JoinBlock<,>.DebugView))] public sealed class JoinBlock : IReceivableSourceBlock>, IDebuggerDisplay { /// Resources shared by all targets for this join block. private readonly JoinBlockTargetSharedResources _sharedResources; /// The source half of this join. private readonly SourceCore> _source; /// The first target. private readonly JoinBlockTarget _target1; /// The second target. private readonly JoinBlockTarget _target2; /// Initializes the . public JoinBlock() : this(GroupingDataflowBlockOptions.Default) { } /// Initializes the . /// The options with which to configure this . /// The is null (Nothing in Visual Basic). public JoinBlock(GroupingDataflowBlockOptions dataflowBlockOptions) { // Validate arguments if (dataflowBlockOptions == null) throw new ArgumentNullException("dataflowBlockOptions"); Contract.EndContractBlock(); // Ensure we have options that can't be changed by the caller dataflowBlockOptions = dataflowBlockOptions.DefaultOrClone(); // Initialize bounding state if necessary Action>, int> onItemsRemoved = null; if (dataflowBlockOptions.BoundedCapacity > 0) onItemsRemoved = (owningSource, count) => ((JoinBlock)owningSource)._sharedResources.OnItemsRemoved(count); // Configure the source _source = new SourceCore>(this, dataflowBlockOptions, owningSource => ((JoinBlock)owningSource)._sharedResources.CompleteEachTarget(), onItemsRemoved); // Configure targets var targets = new JoinBlockTargetBase[2]; _sharedResources = new JoinBlockTargetSharedResources(this, targets, () => { _source.AddMessage(Tuple.Create(_target1.GetOneMessage(), _target2.GetOneMessage())); }, exception => { Volatile.Write(ref _sharedResources._hasExceptions, true); _source.AddException(exception); }, dataflowBlockOptions); targets[0] = _target1 = new JoinBlockTarget(_sharedResources); targets[1] = _target2 = new JoinBlockTarget(_sharedResources); // Let the source know when all targets have completed Task.Factory.ContinueWhenAll( new[] { _target1.CompletionTaskInternal, _target2.CompletionTaskInternal }, _ => _source.Complete(), CancellationToken.None, Common.GetContinuationOptions(), TaskScheduler.Default); // It is possible that the source half may fault on its own, e.g. due to a task scheduler exception. // In those cases we need to fault the target half to drop its buffered messages and to release its // reservations. This should not create an infinite loop, because all our implementations are designed // to handle multiple completion requests and to carry over only one. _source.Completion.ContinueWith((completed, state) => { var thisBlock = ((JoinBlock)state) as IDataflowBlock; Debug.Assert(completed.IsFaulted, "The source must be faulted in order to trigger a target completion."); thisBlock.Fault(completed.Exception); }, this, CancellationToken.None, Common.GetContinuationOptions() | TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Default); // Handle async cancellation requests by declining on the target Common.WireCancellationToComplete( dataflowBlockOptions.CancellationToken, _source.Completion, state => ((JoinBlock)state)._sharedResources.CompleteEachTarget(), this); #if FEATURE_TRACING DataflowEtwProvider etwLog = DataflowEtwProvider.Log; if (etwLog.IsEnabled()) { etwLog.DataflowBlockCreated(this, dataflowBlockOptions); } #endif } /// public IDisposable LinkTo(ITargetBlock> target, DataflowLinkOptions linkOptions) { return _source.LinkTo(target, linkOptions); } /// public Boolean TryReceive(Predicate> filter, out Tuple item) { return _source.TryReceive(filter, out item); } /// [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")] public bool TryReceiveAll(out IList> items) { return _source.TryReceiveAll(out items); } /// public int OutputCount { get { return _source.OutputCount; } } /// public Task Completion { get { return _source.Completion; } } /// public void Complete() { Debug.Assert(_target1 != null, "_target1 not initialized"); Debug.Assert(_target2 != null, "_target2 not initialized"); _target1.CompleteCore(exception: null, dropPendingMessages: false, releaseReservedMessages: false); _target2.CompleteCore(exception: null, dropPendingMessages: false, releaseReservedMessages: false); } /// void IDataflowBlock.Fault(Exception exception) { if (exception == null) throw new ArgumentNullException("exception"); Contract.EndContractBlock(); Debug.Assert(_sharedResources != null, "_sharedResources not initialized"); Debug.Assert(_sharedResources._exceptionAction != null, "_sharedResources._exceptionAction not initialized"); lock (_sharedResources.IncomingLock) { if (!_sharedResources._decliningPermanently) _sharedResources._exceptionAction(exception); } Complete(); } /// Gets a target that may be used to offer messages of the first type. public ITargetBlock Target1 { get { return _target1; } } /// Gets a target that may be used to offer messages of the second type. public ITargetBlock Target2 { get { return _target2; } } /// Tuple ISourceBlock>.ConsumeMessage(DataflowMessageHeader messageHeader, ITargetBlock> target, out Boolean messageConsumed) { return _source.ConsumeMessage(messageHeader, target, out messageConsumed); } /// bool ISourceBlock>.ReserveMessage(DataflowMessageHeader messageHeader, ITargetBlock> target) { return _source.ReserveMessage(messageHeader, target); } /// void ISourceBlock>.ReleaseReservation(DataflowMessageHeader messageHeader, ITargetBlock> target) { _source.ReleaseReservation(messageHeader, target); } /// Gets the number of messages waiting to be processed. This must only be used from the debugger as it avoids taking necessary locks. private int OutputCountForDebugger { get { return _source.GetDebuggingInformation().OutputCount; } } /// public override string ToString() { return Common.GetNameForDebugger(this, _source.DataflowBlockOptions); } /// The data to display in the debugger display attribute. [SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider")] private object DebuggerDisplayContent { get { return string.Format("{0}, OutputCount={1}", Common.GetNameForDebugger(this, _source.DataflowBlockOptions), OutputCountForDebugger); } } /// Gets the data to display in the debugger display attribute for this instance. object IDebuggerDisplay.Content { get { return DebuggerDisplayContent; } } /// Provides a debugger type proxy for the JoinBlock. private sealed class DebugView { /// The JoinBlock being viewed. private readonly JoinBlock _joinBlock; /// The source half of the block being viewed. private readonly SourceCore>.DebuggingInformation _sourceDebuggingInformation; /// Initializes the debug view. /// The JoinBlock being viewed. public DebugView(JoinBlock joinBlock) { Contract.Requires(joinBlock != null, "Need a block with which to construct the debug view."); _joinBlock = joinBlock; _sourceDebuggingInformation = joinBlock._source.GetDebuggingInformation(); } /// Gets the messages waiting to be received. public IEnumerable> OutputQueue { get { return _sourceDebuggingInformation.OutputQueue; } } /// Gets the number of joins created thus far. public long JoinsCreated { get { return _joinBlock._sharedResources._joinsCreated; } } /// Gets the task being used for input processing. public Task TaskForInputProcessing { get { return _joinBlock._sharedResources._taskForInputProcessing; } } /// Gets the task being used for output processing. public Task TaskForOutputProcessing { get { return _sourceDebuggingInformation.TaskForOutputProcessing; } } /// Gets the GroupingDataflowBlockOptions used to configure this block. public GroupingDataflowBlockOptions DataflowBlockOptions { get { return (GroupingDataflowBlockOptions)_sourceDebuggingInformation.DataflowBlockOptions; } } /// Gets whether the block is declining further messages. public bool IsDecliningPermanently { get { return _joinBlock._sharedResources._decliningPermanently; } } /// Gets whether the block is completed. public bool IsCompleted { get { return _sourceDebuggingInformation.IsCompleted; } } /// Gets the block's Id. public int Id { get { return Common.GetBlockId(_joinBlock); } } /// Gets the first target. public ITargetBlock Target1 { get { return _joinBlock._target1; } } /// Gets the second target. public ITargetBlock Target2 { get { return _joinBlock._target2; } } /// Gets the set of all targets linked from this block. public TargetRegistry> LinkedTargets { get { return _sourceDebuggingInformation.LinkedTargets; } } /// Gets the set of all targets linked from this block. public ITargetBlock> NextMessageReservedFor { get { return _sourceDebuggingInformation.NextMessageReservedFor; } } } } /// /// Provides a dataflow block that joins across multiple dataflow sources, not necessarily of the same type, /// waiting for one item to arrive for each type before they’re all released together as a tuple of one item per type. /// /// Specifies the type of data accepted by the block's first target. /// Specifies the type of data accepted by the block's second target. /// Specifies the type of data accepted by the block's third target. [DebuggerDisplay("{DebuggerDisplayContent,nq}")] [DebuggerTypeProxy(typeof(JoinBlock<,,>.DebugView))] [SuppressMessage("Microsoft.Design", "CA1005:AvoidExcessiveParametersOnGenericTypes")] public sealed class JoinBlock : IReceivableSourceBlock>, IDebuggerDisplay { /// Resources shared by all targets for this join block. private readonly JoinBlockTargetSharedResources _sharedResources; /// The source half of this join. private readonly SourceCore> _source; /// The first target. private readonly JoinBlockTarget _target1; /// The second target. private readonly JoinBlockTarget _target2; /// The third target. private readonly JoinBlockTarget _target3; /// Initializes the . public JoinBlock() : this(GroupingDataflowBlockOptions.Default) { } /// Initializes the . /// The options with which to configure this . /// The is null (Nothing in Visual Basic). public JoinBlock(GroupingDataflowBlockOptions dataflowBlockOptions) { // Validate arguments if (dataflowBlockOptions == null) throw new ArgumentNullException("dataflowBlockOptions"); Contract.EndContractBlock(); // Ensure we have options that can't be changed by the caller dataflowBlockOptions = dataflowBlockOptions.DefaultOrClone(); // Initialize bounding state if necessary Action>, int> onItemsRemoved = null; if (dataflowBlockOptions.BoundedCapacity > 0) onItemsRemoved = (owningSource, count) => ((JoinBlock)owningSource)._sharedResources.OnItemsRemoved(count); // Configure the source _source = new SourceCore>(this, dataflowBlockOptions, owningSource => ((JoinBlock)owningSource)._sharedResources.CompleteEachTarget(), onItemsRemoved); // Configure the targets var targets = new JoinBlockTargetBase[3]; _sharedResources = new JoinBlockTargetSharedResources(this, targets, () => _source.AddMessage(Tuple.Create(_target1.GetOneMessage(), _target2.GetOneMessage(), _target3.GetOneMessage())), exception => { Volatile.Write(ref _sharedResources._hasExceptions, true); _source.AddException(exception); }, dataflowBlockOptions); targets[0] = _target1 = new JoinBlockTarget(_sharedResources); targets[1] = _target2 = new JoinBlockTarget(_sharedResources); targets[2] = _target3 = new JoinBlockTarget(_sharedResources); // Let the source know when all targets have completed Task.Factory.ContinueWhenAll( new[] { _target1.CompletionTaskInternal, _target2.CompletionTaskInternal, _target3.CompletionTaskInternal }, _ => _source.Complete(), CancellationToken.None, Common.GetContinuationOptions(), TaskScheduler.Default); // It is possible that the source half may fault on its own, e.g. due to a task scheduler exception. // In those cases we need to fault the target half to drop its buffered messages and to release its // reservations. This should not create an infinite loop, because all our implementations are designed // to handle multiple completion requests and to carry over only one. _source.Completion.ContinueWith((completed, state) => { var thisBlock = ((JoinBlock)state) as IDataflowBlock; Debug.Assert(completed.IsFaulted, "The source must be faulted in order to trigger a target completion."); thisBlock.Fault(completed.Exception); }, this, CancellationToken.None, Common.GetContinuationOptions() | TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Default); // Handle async cancellation requests by declining on the target Common.WireCancellationToComplete( dataflowBlockOptions.CancellationToken, _source.Completion, state => ((JoinBlock)state)._sharedResources.CompleteEachTarget(), this); #if FEATURE_TRACING DataflowEtwProvider etwLog = DataflowEtwProvider.Log; if (etwLog.IsEnabled()) { etwLog.DataflowBlockCreated(this, dataflowBlockOptions); } #endif } /// public IDisposable LinkTo(ITargetBlock> target, DataflowLinkOptions linkOptions) { return _source.LinkTo(target, linkOptions); } /// public Boolean TryReceive(Predicate> filter, out Tuple item) { return _source.TryReceive(filter, out item); } /// [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")] public bool TryReceiveAll(out IList> items) { return _source.TryReceiveAll(out items); } /// public int OutputCount { get { return _source.OutputCount; } } /// public Task Completion { get { return _source.Completion; } } /// public void Complete() { Debug.Assert(_target1 != null, "_target1 not initialized"); Debug.Assert(_target2 != null, "_target2 not initialized"); Debug.Assert(_target3 != null, "_target3 not initialized"); _target1.CompleteCore(exception: null, dropPendingMessages: false, releaseReservedMessages: false); _target2.CompleteCore(exception: null, dropPendingMessages: false, releaseReservedMessages: false); _target3.CompleteCore(exception: null, dropPendingMessages: false, releaseReservedMessages: false); } /// void IDataflowBlock.Fault(Exception exception) { if (exception == null) throw new ArgumentNullException("exception"); Contract.EndContractBlock(); Debug.Assert(_sharedResources != null, "_sharedResources not initialized"); Debug.Assert(_sharedResources._exceptionAction != null, "_sharedResources._exceptionAction not initialized"); lock (_sharedResources.IncomingLock) { if (!_sharedResources._decliningPermanently) _sharedResources._exceptionAction(exception); } Complete(); } /// Gets a target that may be used to offer messages of the first type. public ITargetBlock Target1 { get { return _target1; } } /// Gets a target that may be used to offer messages of the second type. public ITargetBlock Target2 { get { return _target2; } } /// Gets a target that may be used to offer messages of the third type. public ITargetBlock Target3 { get { return _target3; } } /// Tuple ISourceBlock>.ConsumeMessage(DataflowMessageHeader messageHeader, ITargetBlock> target, out Boolean messageConsumed) { return _source.ConsumeMessage(messageHeader, target, out messageConsumed); } /// bool ISourceBlock>.ReserveMessage(DataflowMessageHeader messageHeader, ITargetBlock> target) { return _source.ReserveMessage(messageHeader, target); } /// void ISourceBlock>.ReleaseReservation(DataflowMessageHeader messageHeader, ITargetBlock> target) { _source.ReleaseReservation(messageHeader, target); } /// Gets the number of messages waiting to be processed. This must only be used from the debugger as it avoids taking necessary locks. private int OutputCountForDebugger { get { return _source.GetDebuggingInformation().OutputCount; } } /// public override string ToString() { return Common.GetNameForDebugger(this, _source.DataflowBlockOptions); } /// The data to display in the debugger display attribute. [SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider")] private object DebuggerDisplayContent { get { return string.Format("{0} OutputCount={1}", Common.GetNameForDebugger(this, _source.DataflowBlockOptions), OutputCountForDebugger); } } /// Gets the data to display in the debugger display attribute for this instance. object IDebuggerDisplay.Content { get { return DebuggerDisplayContent; } } /// Provides a debugger type proxy for the Batch. private sealed class DebugView { /// The JoinBlock being viewed. private readonly JoinBlock _joinBlock; /// The source half of the block being viewed. private readonly SourceCore>.DebuggingInformation _sourceDebuggingInformation; /// Initializes the debug view. /// The JoinBlock being viewed. public DebugView(JoinBlock joinBlock) { Contract.Requires(joinBlock != null, "Need a block with which to construct the debug view."); _joinBlock = joinBlock; _sourceDebuggingInformation = joinBlock._source.GetDebuggingInformation(); } /// Gets the messages waiting to be received. public IEnumerable> OutputQueue { get { return _sourceDebuggingInformation.OutputQueue; } } /// Gets the number of joins created thus far. public long JoinsCreated { get { return _joinBlock._sharedResources._joinsCreated; } } /// Gets the task being used for input processing. public Task TaskForInputProcessing { get { return _joinBlock._sharedResources._taskForInputProcessing; } } /// Gets the task being used for output processing. public Task TaskForOutputProcessing { get { return _sourceDebuggingInformation.TaskForOutputProcessing; } } /// Gets the GroupingDataflowBlockOptions used to configure this block. public GroupingDataflowBlockOptions DataflowBlockOptions { get { return (GroupingDataflowBlockOptions)_sourceDebuggingInformation.DataflowBlockOptions; } } /// Gets whether the block is declining further messages. public bool IsDecliningPermanently { get { return _joinBlock._sharedResources._decliningPermanently; } } /// Gets whether the block is completed. public bool IsCompleted { get { return _sourceDebuggingInformation.IsCompleted; } } /// Gets the block's Id. public int Id { get { return Common.GetBlockId(_joinBlock); } } /// Gets the first target. public ITargetBlock Target1 { get { return _joinBlock._target1; } } /// Gets the second target. public ITargetBlock Target2 { get { return _joinBlock._target2; } } /// Gets the third target. public ITargetBlock Target3 { get { return _joinBlock._target3; } } /// Gets the set of all targets linked from this block. public TargetRegistry> LinkedTargets { get { return _sourceDebuggingInformation.LinkedTargets; } } /// Gets the set of all targets linked from this block. public ITargetBlock> NextMessageReservedFor { get { return _sourceDebuggingInformation.NextMessageReservedFor; } } } } } namespace System.Threading.Tasks.Dataflow.Internal { /// Provides the target used in a Join. /// Specifies the type of data accepted by this target. [DebuggerDisplay("{DebuggerDisplayContent,nq}")] [DebuggerTypeProxy(typeof(JoinBlockTarget<>.DebugView))] internal sealed class JoinBlockTarget : JoinBlockTargetBase, ITargetBlock, IDebuggerDisplay { /// The shared resources used by all targets associated with the same join instance. private readonly JoinBlockTargetSharedResources _sharedResources; /// A task representing the completion of the block. private readonly TaskCompletionSource _completionTask = new TaskCompletionSource(); /// Input messages for the next batch. private readonly Queue _messages; /// State used when in non-greedy mode. private readonly NonGreedyState _nonGreedy; /// Whether this target is declining future messages. private bool _decliningPermanently; /// State used only when in non-greedy mode. private sealed class NonGreedyState { /// Collection of the last postponed message per source. internal readonly QueuedMap, DataflowMessageHeader> PostponedMessages = new QueuedMap, DataflowMessageHeader>(); /// The currently reserved message. internal KeyValuePair, DataflowMessageHeader> ReservedMessage; /// The currently consumed message. internal KeyValuePair ConsumedMessage; } /// Initializes the target. /// The shared resources used by all targets associated with this join. internal JoinBlockTarget(JoinBlockTargetSharedResources sharedResources) { Contract.Requires(sharedResources != null, "Targets need shared resources through which to communicate."); // Store arguments and initialize configuration GroupingDataflowBlockOptions dbo = sharedResources._dataflowBlockOptions; _sharedResources = sharedResources; if (!dbo.Greedy || dbo.BoundedCapacity > 0) _nonGreedy = new NonGreedyState(); if (dbo.Greedy) _messages = new Queue(); } /// Gets a message buffered by this target. /// This must be called while holding the shared Resources's incoming lock. internal T GetOneMessage() { Common.ContractAssertMonitorStatus(_sharedResources.IncomingLock, held: true); if (_sharedResources._dataflowBlockOptions.Greedy) { Debug.Assert(_messages != null, "_messages must have been initialized in greedy mode"); Debug.Assert(_messages.Count >= 0, "A message must have been consumed by this point."); return _messages.Dequeue(); } else { Debug.Assert(_nonGreedy.ConsumedMessage.Key, "A message must have been consumed by this point."); T value = _nonGreedy.ConsumedMessage.Value; _nonGreedy.ConsumedMessage = new KeyValuePair(false, default(T)); return value; } } /// Gets whether the target is declining messages. internal override bool IsDecliningPermanently { get { Common.ContractAssertMonitorStatus(_sharedResources.IncomingLock, held: true); return _decliningPermanently; } } /// Gets whether the target has at least one message available. internal override bool HasAtLeastOneMessageAvailable { get { Common.ContractAssertMonitorStatus(_sharedResources.IncomingLock, held: true); if (_sharedResources._dataflowBlockOptions.Greedy) { Debug.Assert(_messages != null, "_messages must have been initialized in greedy mode"); return _messages.Count > 0; } else { return _nonGreedy.ConsumedMessage.Key; } } } /// Gets whether the target has at least one postponed message. internal override bool HasAtLeastOnePostponedMessage { get { Common.ContractAssertMonitorStatus(_sharedResources.IncomingLock, held: true); return _nonGreedy != null && _nonGreedy.PostponedMessages.Count > 0; } } /// Gets the number of messages available or postponed. internal override int NumberOfMessagesAvailableOrPostponed { get { Common.ContractAssertMonitorStatus(_sharedResources.IncomingLock, held: true); return !_sharedResources._dataflowBlockOptions.Greedy ? _nonGreedy.PostponedMessages.Count : _messages.Count; } } /// Gets whether this target has the highest number of available/buffered messages. This is only valid in greedy mode. internal override bool HasTheHighestNumberOfMessagesAvailable { get { Debug.Assert(_sharedResources._dataflowBlockOptions.Greedy, "This is only valid in greedy mode"); Common.ContractAssertMonitorStatus(_sharedResources.IncomingLock, held: true); // Note: If there is a tie, we must return true int count = _messages.Count; foreach (JoinBlockTargetBase target in _sharedResources._targets) if (target != this && target.NumberOfMessagesAvailableOrPostponed > count) return false; // Strictly bigger! return true; } } /// Reserves one of the postponed messages. /// true if a message was reserved; otherwise, false. internal override bool ReserveOneMessage() { Common.ContractAssertMonitorStatus(_sharedResources.IncomingLock, held: false); Debug.Assert(!_sharedResources._dataflowBlockOptions.Greedy, "This is only used in non-greedy mode"); KeyValuePair, DataflowMessageHeader> next; lock (_sharedResources.IncomingLock) { // The queue must be empty between joins in non-greedy mode Debug.Assert(!HasAtLeastOneMessageAvailable, "The queue must be empty between joins in non-greedy mode"); // While we are holding the lock, try to pop a postponed message. // If there are no postponed messages, we can't do anything. if (!_nonGreedy.PostponedMessages.TryPop(out next)) return false; } // We'll bail out of this loop either when we have reserved a message (true) // or when we have exhausted the list of postponed messages (false) for (; ;) { // Try to reserve the popped message if (next.Key.ReserveMessage(next.Value, this)) { _nonGreedy.ReservedMessage = next; return true; } // We could not reserve that message. // Try to pop another postponed message and continue looping. lock (_sharedResources.IncomingLock) { // If there are no postponed messages, we can't do anything if (!_nonGreedy.PostponedMessages.TryPop(out next)) return false; } } } /// Consumes a reserved message. /// true if a message was consumed; otherwise, false. internal override bool ConsumeReservedMessage() { Common.ContractAssertMonitorStatus(_sharedResources.IncomingLock, held: false); Debug.Assert(!_sharedResources._dataflowBlockOptions.Greedy, "This is only used in non-greedy mode"); Debug.Assert(_nonGreedy.ReservedMessage.Key != null, "This target must have a reserved message"); bool consumed; T consumedValue = _nonGreedy.ReservedMessage.Key.ConsumeMessage(_nonGreedy.ReservedMessage.Value, this, out consumed); // Null out our reservation _nonGreedy.ReservedMessage = default(KeyValuePair, DataflowMessageHeader>); // The protocol requires that a reserved message must be consumable, // but it is possible that the source may misbehave. // In that case complete the target and signal to the owning block to shut down gracefully. if (!consumed) { _sharedResources._exceptionAction(new InvalidOperationException(SR.InvalidOperation_FailedToConsumeReservedMessage)); // Complete this target, which will trigger completion of the owning join block. CompleteOncePossible(); // We need to signal to the caller to stop consuming immediately return false; } else { lock (_sharedResources.IncomingLock) { // Now that we've consumed it, store its data. Debug.Assert(!_nonGreedy.ConsumedMessage.Key, "There must be no other consumed message"); _nonGreedy.ConsumedMessage = new KeyValuePair(true, consumedValue); // We don't account bounding per target in non-greedy mode. We do it once per batch (in the loop). CompleteIfLastJoinIsFeasible(); } } return true; } /// Consumes up to one postponed message in greedy bounded mode. internal override bool ConsumeOnePostponedMessage() { Common.ContractAssertMonitorStatus(_sharedResources.IncomingLock, held: false); Debug.Assert(_sharedResources._dataflowBlockOptions.Greedy, "This is only used in greedy mode"); Debug.Assert(_sharedResources._boundingState != null, "This is only used in bounding mode"); // We'll bail out of this loop either when we have consumed a message (true) // or when we have exhausted the list of postponed messages (false) while (true) { KeyValuePair, DataflowMessageHeader> next; bool hasTheHighestNumberOfMessagesAvailable; lock (_sharedResources.IncomingLock) { // While we are holding the lock, check bounding capacity and try to pop a postponed message. // If anything fails, we can't do anything. hasTheHighestNumberOfMessagesAvailable = HasTheHighestNumberOfMessagesAvailable; bool boundingCapacityAvailable = _sharedResources._boundingState.CountIsLessThanBound || !hasTheHighestNumberOfMessagesAvailable; if (_decliningPermanently || _sharedResources._decliningPermanently || !boundingCapacityAvailable || !_nonGreedy.PostponedMessages.TryPop(out next)) return false; } // Try to consume the popped message bool consumed; T consumedValue = next.Key.ConsumeMessage(next.Value, this, out consumed); if (consumed) { lock (_sharedResources.IncomingLock) { // The ranking in highest number of available messages cannot have changed because this task is causing OfferMessage to postpone if (hasTheHighestNumberOfMessagesAvailable) _sharedResources._boundingState.CurrentCount += 1; // track this new item against our bound _messages.Enqueue(consumedValue); CompleteIfLastJoinIsFeasible(); return true; } } } } /// /// Start declining if the number of joins we've already made plus the number we can /// make from data already enqueued meets our quota. /// private void CompleteIfLastJoinIsFeasible() { Common.ContractAssertMonitorStatus(_sharedResources.IncomingLock, held: true); int messageCount = _sharedResources._dataflowBlockOptions.Greedy ? _messages.Count : _nonGreedy.ConsumedMessage.Key ? 1 : 0; if ((_sharedResources._joinsCreated + messageCount) >= _sharedResources._dataflowBlockOptions.ActualMaxNumberOfGroups) { _decliningPermanently = true; bool allAreDecliningPermanently = true; foreach (JoinBlockTargetBase target in _sharedResources._targets) { if (!target.IsDecliningPermanently) { allAreDecliningPermanently = false; break; } } if (allAreDecliningPermanently) _sharedResources._decliningPermanently = true; } } /// Releases the reservation on a reserved message. internal override void ReleaseReservedMessage() { Common.ContractAssertMonitorStatus(_sharedResources.IncomingLock, held: false); // Release only if we have a reserved message. // Otherwise do nothing. if (_nonGreedy != null && _nonGreedy.ReservedMessage.Key != null) { // Release the reservation and null out our reservation flag even if an exception occurs try { _nonGreedy.ReservedMessage.Key.ReleaseReservation(_nonGreedy.ReservedMessage.Value, this); } finally { ClearReservation(); } } } /// Unconditionally clears a reserved message. internal override void ClearReservation() { Common.ContractAssertMonitorStatus(_sharedResources.IncomingLock, held: false); Debug.Assert(_nonGreedy != null, "Only valid in non-greedy mode."); _nonGreedy.ReservedMessage = default(KeyValuePair, DataflowMessageHeader>); } /// Completes the target. internal override void CompleteOncePossible() { Common.ContractAssertMonitorStatus(_sharedResources.IncomingLock, held: false); // This target must not have an outstanding reservation Debug.Assert(_nonGreedy == null || _nonGreedy.ReservedMessage.Key == null, "Must be in greedy mode, or in non-greedy mode but without any reserved messages."); // Clean up any messages that may be stragglers left behind lock (_sharedResources.IncomingLock) { _decliningPermanently = true; if (_messages != null) _messages.Clear(); } // Release any postponed messages List exceptions = null; if (_nonGreedy != null) { // Note: No locks should be held at this point Common.ReleaseAllPostponedMessages(this, _nonGreedy.PostponedMessages, ref exceptions); } if (exceptions != null) { // It is important to migrate these exceptions to the source part of the owning join, // because that is the completion task that is publically exposed. foreach (Exception exc in exceptions) { _sharedResources._exceptionAction(exc); } } // Targets' completion tasks are only available internally with the sole purpose // of releasing the task that completes the parent. Hence the actual reason // for completing this task doesn't matter. _completionTask.TrySetResult(default(VoidResult)); } /// DataflowMessageStatus ITargetBlock.OfferMessage(DataflowMessageHeader messageHeader, T messageValue, ISourceBlock source, Boolean consumeToAccept) { // Validate arguments if (!messageHeader.IsValid) throw new ArgumentException(SR.Argument_InvalidMessageHeader, "messageHeader"); if (source == null && consumeToAccept) throw new ArgumentException(SR.Argument_CantConsumeFromANullSource, "consumeToAccept"); Contract.EndContractBlock(); lock (_sharedResources.IncomingLock) { // If we shouldn't be accepting more messages, don't. if (_decliningPermanently || _sharedResources._decliningPermanently) { _sharedResources.CompleteBlockIfPossible(); return DataflowMessageStatus.DecliningPermanently; } // We can directly accept the message if: // 1) we are being greedy AND we are not bounding, OR // 2) we are being greedy AND we are bounding AND there is room available AND there are no postponed messages AND we are not currently processing. // (If there were any postponed messages, we would need to postpone so that ordering would be maintained.) // (We should also postpone if we are currently processing, because there may be a race between consuming postponed messages and // accepting new ones directly into the queue.) if (_sharedResources._dataflowBlockOptions.Greedy && (_sharedResources._boundingState == null || ((_sharedResources._boundingState.CountIsLessThanBound || !HasTheHighestNumberOfMessagesAvailable) && _nonGreedy.PostponedMessages.Count == 0 && _sharedResources._taskForInputProcessing == null))) { if (consumeToAccept) { Debug.Assert(source != null, "We must have thrown if source == null && consumeToAccept == true."); bool consumed; messageValue = source.ConsumeMessage(messageHeader, this, out consumed); if (!consumed) return DataflowMessageStatus.NotAvailable; } if (_sharedResources._boundingState != null && HasTheHighestNumberOfMessagesAvailable) _sharedResources._boundingState.CurrentCount += 1; // track this new item against our bound _messages.Enqueue(messageValue); CompleteIfLastJoinIsFeasible(); // Since we're in greedy mode, we can skip asynchronous processing and // make joins aggressively based on enqueued data. if (_sharedResources.AllTargetsHaveAtLeastOneMessage) { _sharedResources._joinFilledAction(); _sharedResources._joinsCreated++; } _sharedResources.CompleteBlockIfPossible(); return DataflowMessageStatus.Accepted; } // Otherwise, we try to postpone if a source was provided else if (source != null) { Debug.Assert(_nonGreedy != null, "_nonGreedy must have been initialized during construction in non-greedy mode."); // Postpone the message now and kick off an async two-phase consumption. _nonGreedy.PostponedMessages.Push(source, messageHeader); _sharedResources.ProcessAsyncIfNecessary(); return DataflowMessageStatus.Postponed; } // We can't do anything else about this message return DataflowMessageStatus.Declined; } } /// Completes/faults the block. /// In general, it is not safe to pass releaseReservedMessages:true, because releasing of reserved messages /// is done without taking a lock. We pass releaseReservedMessages:true only when an exception has been /// caught inside the message processing loop which is a single instance at any given moment. [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] internal override void CompleteCore(Exception exception, bool dropPendingMessages, bool releaseReservedMessages) { bool greedy = _sharedResources._dataflowBlockOptions.Greedy; lock (_sharedResources.IncomingLock) { // Faulting from outside is allowed until we start declining permanently. // Faulting from inside is allowed at any time. if (exception != null && ((!_decliningPermanently && !_sharedResources._decliningPermanently) || releaseReservedMessages)) { _sharedResources._exceptionAction(exception); } // Drop pending messages if requested if (dropPendingMessages && greedy) { Debug.Assert(_messages != null, "_messages must be initialized in greedy mode."); _messages.Clear(); } } // Release reserved messages if requested. // This must be done from outside the lock. if (releaseReservedMessages && !greedy) { // Do this on all targets foreach (JoinBlockTargetBase target in _sharedResources._targets) { try { target.ReleaseReservedMessage(); } catch (Exception e) { _sharedResources._exceptionAction(e); } } } // Triggering completion requires the lock lock (_sharedResources.IncomingLock) { // Trigger completion _decliningPermanently = true; _sharedResources.CompleteBlockIfPossible(); } } /// void IDataflowBlock.Fault(Exception exception) { if (exception == null) throw new ArgumentNullException("exception"); Contract.EndContractBlock(); CompleteCore(exception, dropPendingMessages: true, releaseReservedMessages: false); } /// public Task Completion { get { throw new NotSupportedException(SR.NotSupported_MemberNotNeeded); } } /// The completion task on Join targets is only hidden from the public. It still exists for internal purposes. internal Task CompletionTaskInternal { get { return _completionTask.Task; } } /// Gets the number of messages waiting to be processed. This must only be used from the debugger as it avoids taking necessary locks. private int InputCountForDebugger { get { return _messages != null ? _messages.Count : _nonGreedy.ConsumedMessage.Key ? 1 : 0; } } /// The data to display in the debugger display attribute. [SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider")] private object DebuggerDisplayContent { get { var displayJoin = _sharedResources._ownerJoin as IDebuggerDisplay; return string.Format("{0} InputCount={1}, Join=\"{2}\"", Common.GetNameForDebugger(this), InputCountForDebugger, displayJoin != null ? displayJoin.Content : _sharedResources._ownerJoin); } } /// Gets the data to display in the debugger display attribute for this instance. object IDebuggerDisplay.Content { get { return DebuggerDisplayContent; } } /// Provides a debugger type proxy for the Transform. private sealed class DebugView { /// The join block target being viewed. private readonly JoinBlockTarget _joinBlockTarget; /// Initializes the debug view. /// The join being viewed. public DebugView(JoinBlockTarget joinBlockTarget) { Contract.Requires(joinBlockTarget != null, "Need a target with which to construct the debug view."); _joinBlockTarget = joinBlockTarget; } /// Gets the messages waiting to be processed. public IEnumerable InputQueue { get { return _joinBlockTarget._messages; } } /// Gets whether the block is declining further messages. public bool IsDecliningPermanently { get { return _joinBlockTarget._decliningPermanently || _joinBlockTarget._sharedResources._decliningPermanently; } } } } /// Provides a non-generic base type for all join targets. internal abstract class JoinBlockTargetBase { /// Whether the target is postponing messages. internal abstract bool IsDecliningPermanently { get; } /// Whether the target has at least one message available. internal abstract bool HasAtLeastOneMessageAvailable { get; } /// Whether the target has at least one message postponed. internal abstract bool HasAtLeastOnePostponedMessage { get; } /// Gets the number of messages available or postponed. internal abstract int NumberOfMessagesAvailableOrPostponed { get; } /// Gets whether the target has the highest number of messages available. (A tie yields true.) internal abstract bool HasTheHighestNumberOfMessagesAvailable { get; } /// Reserves a single message. /// Whether a message was reserved. internal abstract bool ReserveOneMessage(); /// Consumes any previously reserved message. /// Whether a message was consumed. internal abstract bool ConsumeReservedMessage(); /// Consumes up to one postponed message in greedy bounded mode. /// Whether a message was consumed. internal abstract bool ConsumeOnePostponedMessage(); /// Releases any previously reserved message. internal abstract void ReleaseReservedMessage(); /// Unconditionally clears a reserved message. This is only invoked in case of an exception. internal abstract void ClearReservation(); /// Access point to the corresponding API method. public void Complete() { CompleteCore(exception: null, dropPendingMessages: false, releaseReservedMessages: false); } /// Internal implementation of the corresponding API method. internal abstract void CompleteCore(Exception exception, bool dropPendingMessages, bool releaseReservedMessages); /// Completes the target. internal abstract void CompleteOncePossible(); } /// Provides a container for resources shared across all targets used by the same BatchedJoin instance. [SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable")] [DebuggerDisplay("{DebuggerDisplayContent,nq}")] internal sealed class JoinBlockTargetSharedResources { /// Initializes the shared resources. /// /// The join block that owns these shared resources. /// /// /// The array of targets associated with the join. Also doubles as the sync object used to synchronize targets. /// /// The delegate to invoke when enough messages have been consumed to fulfill the join. /// The delegate to invoke when a target encounters an error. /// The options for the join. internal JoinBlockTargetSharedResources( IDataflowBlock ownerJoin, JoinBlockTargetBase[] targets, Action joinFilledAction, Action exceptionAction, GroupingDataflowBlockOptions dataflowBlockOptions) { Contract.Requires(ownerJoin != null, "Resources must be associated with a join."); Contract.Requires(targets != null, "Resources must be shared between multiple targets."); Contract.Requires(joinFilledAction != null, "An action to invoke when a join is created must be provided."); Contract.Requires(exceptionAction != null, "An action to invoke for faults must be provided."); Contract.Requires(dataflowBlockOptions != null, "Options must be provided to configure the resources."); // Store arguments _ownerJoin = ownerJoin; _targets = targets; _joinFilledAction = joinFilledAction; _exceptionAction = exceptionAction; _dataflowBlockOptions = dataflowBlockOptions; // Initialize bounding state if necessary if (dataflowBlockOptions.BoundedCapacity > 0) _boundingState = new BoundingState(dataflowBlockOptions.BoundedCapacity); } // *** Accessible fields and properties internal readonly IDataflowBlock _ownerJoin; /// All of the targets associated with the join. internal readonly JoinBlockTargetBase[] _targets; /// The delegate to invoke when a target encounters an error. internal readonly Action _exceptionAction; /// The delegate to invoke when enough messages have been consumed to fulfill the join. internal readonly Action _joinFilledAction; /// The options for the join. internal readonly GroupingDataflowBlockOptions _dataflowBlockOptions; /// Bounding state for when the block is executing in bounded mode. internal readonly BoundingState _boundingState; /// Whether all targets should decline all further messages. internal bool _decliningPermanently; /// The task used to process messages. internal Task _taskForInputProcessing; /// Whether any exceptions have been generated and stored into the source core. internal bool _hasExceptions; /// The number of joins this block has created. internal long _joinsCreated; // *** Private fields and properties - mutatable /// A task has reserved the right to run the completion routine. private bool _completionReserved; /// Gets the lock used to synchronize all incoming messages on all targets. internal object IncomingLock { get { return _targets; } } /// Invokes Complete on each target with dropping buffered messages. internal void CompleteEachTarget() { foreach (JoinBlockTargetBase target in _targets) { target.CompleteCore(exception: null, dropPendingMessages: true, releaseReservedMessages: false); } } /// Gets whether all of the targets have at least one message in their queues. internal bool AllTargetsHaveAtLeastOneMessage { get { Common.ContractAssertMonitorStatus(IncomingLock, held: true); foreach (JoinBlockTargetBase target in _targets) { if (!target.HasAtLeastOneMessageAvailable) return false; } return true; } } /// Gets whether all of the targets have at least one message in their queues or have at least one postponed message. private bool TargetsHaveAtLeastOneMessageQueuedOrPostponed { get { Common.ContractAssertMonitorStatus(IncomingLock, held: true); if (_boundingState == null) { foreach (JoinBlockTargetBase target in _targets) { if (!target.HasAtLeastOneMessageAvailable && (_decliningPermanently || target.IsDecliningPermanently || !target.HasAtLeastOnePostponedMessage)) return false; } return true; } else { // Cache the availability state so we don't evaluate it multiple times bool boundingCapacityAvailable = _boundingState.CountIsLessThanBound; // In bounding mode, we have more complex rules whether we should process input messages: // 1) In greedy mode if a target has postponed messages and there is bounding capacity // available, then we should greedily consume messages up to the bounding capacity // even if that doesn't lead to an output join. // 2) The ability to make join depends on: // 2a) message availability for each target, AND // 2b) availability of bounding space bool joinIsPossible = true; bool joinWillNotAffectBoundingCount = false; foreach (JoinBlockTargetBase target in _targets) { bool targetCanConsumePostponedMessages = !_decliningPermanently && !target.IsDecliningPermanently && target.HasAtLeastOnePostponedMessage; // Rule #1 if (_dataflowBlockOptions.Greedy && targetCanConsumePostponedMessages && (boundingCapacityAvailable || !target.HasTheHighestNumberOfMessagesAvailable)) return true; // Rule #2a bool targetHasMessagesAvailable = target.HasAtLeastOneMessageAvailable; joinIsPossible &= targetHasMessagesAvailable || targetCanConsumePostponedMessages; // Rule #2b // If there is a target that has at least one queued message, bounding space availability // is no longer an issue, because 1 item from the input side will be replaced with 1 // item on the output side. if (targetHasMessagesAvailable) joinWillNotAffectBoundingCount = true; } // Rule #2 return joinIsPossible && (joinWillNotAffectBoundingCount || boundingCapacityAvailable); } } } /// Retrieves postponed items if we have enough to make a batch. /// true if input messages for a batch were consumed (all or none); false otherwise. private bool RetrievePostponedItemsNonGreedy() { Common.ContractAssertMonitorStatus(IncomingLock, held: false); // If there are not enough postponed items, we have nothing to do. lock (IncomingLock) { if (!TargetsHaveAtLeastOneMessageQueuedOrPostponed) return false; } // Release the lock. We must not hold it while calling Reserve/Consume/Release. // Try to reserve a postponed message on every target that doesn't already have messages available bool reservedAll = true; foreach (JoinBlockTargetBase target in _targets) { if (!target.ReserveOneMessage()) { reservedAll = false; break; } } // If we were able to, consume them all and place the consumed messages into each's queue if (reservedAll) { foreach (JoinBlockTargetBase target in _targets) { // If we couldn't consume a message, release reservations wherever possible if (!target.ConsumeReservedMessage()) { reservedAll = false; break; } } } // If we were unable to reserve all messages, release the reservations if (!reservedAll) { foreach (JoinBlockTargetBase target in _targets) { target.ReleaseReservedMessage(); } } return reservedAll; } /// Retrieves up to one postponed item through each target. /// true if at least one input message was consumed (through any target); false otherwise. private bool RetrievePostponedItemsGreedyBounded() { Common.ContractAssertMonitorStatus(IncomingLock, held: false); // Try to consume a postponed message through each target as possible bool consumed = false; foreach (JoinBlockTargetBase target in _targets) { // It is sufficient to consume through one target to consider we've made progress consumed |= target.ConsumeOnePostponedMessage(); } return consumed; } /// Gets whether the target has had cancellation requested or an exception has occurred. private bool CanceledOrFaulted { get { Common.ContractAssertMonitorStatus(IncomingLock, held: true); return _dataflowBlockOptions.CancellationToken.IsCancellationRequested || _hasExceptions; } } /// /// Gets whether the join is in a state where processing can be done, meaning there's data /// to be processed and the block is in a state where such processing is allowed. /// internal bool JoinNeedsProcessing { get { Common.ContractAssertMonitorStatus(IncomingLock, held: true); return _taskForInputProcessing == null && // not currently processing asynchronously !CanceledOrFaulted && // not canceled or faulted TargetsHaveAtLeastOneMessageQueuedOrPostponed; // all targets have work queued or postponed } } /// Called when new messages are available to be processed. /// Whether this call is the continuation of a previous message loop. internal void ProcessAsyncIfNecessary(bool isReplacementReplica = false) { Common.ContractAssertMonitorStatus(IncomingLock, held: true); if (JoinNeedsProcessing) { ProcessAsyncIfNecessary_Slow(isReplacementReplica); } } /// /// Slow path for ProcessAsyncIfNecessary. /// Separating out the slow path into its own method makes it more likely that the fast path method will get inlined. /// private void ProcessAsyncIfNecessary_Slow(bool isReplacementReplica) { Contract.Requires(JoinNeedsProcessing, "There must be a join that needs processing."); Common.ContractAssertMonitorStatus(IncomingLock, held: true); // Create task and store into _taskForInputProcessing prior to scheduling the task // so that _taskForInputProcessing will be visibly set in the task loop. _taskForInputProcessing = new Task(thisSharedResources => ((JoinBlockTargetSharedResources)thisSharedResources).ProcessMessagesLoopCore(), this, Common.GetCreationOptionsForTask(isReplacementReplica)); #if FEATURE_TRACING DataflowEtwProvider etwLog = DataflowEtwProvider.Log; if (etwLog.IsEnabled()) { etwLog.TaskLaunchedForMessageHandling( _ownerJoin, _taskForInputProcessing, DataflowEtwProvider.TaskLaunchedReason.ProcessingInputMessages, _targets.Max(t => t.NumberOfMessagesAvailableOrPostponed)); } #endif // Start the task handling scheduling exceptions Exception exception = Common.StartTaskSafe(_taskForInputProcessing, _dataflowBlockOptions.TaskScheduler); if (exception != null) { // All of the following actions must be performed under the lock. // So do them now while the lock is being held. // First, log the exception while the processing state is dirty which is preventing the block from completing. // Then revert the proactive processing state changes. // And last, try to complete the block. _exceptionAction(exception); _taskForInputProcessing = null; CompleteBlockIfPossible(); } } /// Completes the join block if possible. internal void CompleteBlockIfPossible() { Common.ContractAssertMonitorStatus(IncomingLock, held: true); if (!_completionReserved) { // Check whether we're sure we'll never be able to fill another join. // That could happen if we're not accepting more messages and not all targets have a message... bool impossibleToCompleteAnotherJoin = _decliningPermanently && !AllTargetsHaveAtLeastOneMessage; if (!impossibleToCompleteAnotherJoin) { //...or that could happen if an individual target isn't accepting messages and doesn't have any messages available foreach (JoinBlockTargetBase target in _targets) { if (target.IsDecliningPermanently && !target.HasAtLeastOneMessageAvailable) { impossibleToCompleteAnotherJoin = true; break; } } } // We're done forever if there's no task currently processing and // either it's impossible we'll have another join or we're canceled. bool currentlyProcessing = _taskForInputProcessing != null; bool shouldComplete = !currentlyProcessing && (impossibleToCompleteAnotherJoin || CanceledOrFaulted); if (shouldComplete) { // Make sure no one else tries to call CompleteBlockOncePossible _completionReserved = true; // Make sure all targets are declining _decliningPermanently = true; // Complete each target asynchronously so as not to invoke synchronous continuations under a lock Task.Factory.StartNew(state => { var sharedResources = (JoinBlockTargetSharedResources)state; foreach (JoinBlockTargetBase target in sharedResources._targets) target.CompleteOncePossible(); }, this, CancellationToken.None, Common.GetCreationOptionsForTask(), TaskScheduler.Default); } } } /// Task body used to process messages. [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] private void ProcessMessagesLoopCore() { Contract.Requires(!_dataflowBlockOptions.Greedy || _boundingState != null, "This only makes sense in non-greedy or bounding mode"); Common.ContractAssertMonitorStatus(IncomingLock, held: false); try { int timesThroughLoop = 0; int maxMessagesPerTask = _dataflowBlockOptions.ActualMaxMessagesPerTask; bool madeProgress; do { // Retrieve postponed messages. // In greedy bounded mode, consuming a message through a target is sufficient // to consider we've made progress, i.e. to stay in the loop. madeProgress = !_dataflowBlockOptions.Greedy ? RetrievePostponedItemsNonGreedy() : RetrievePostponedItemsGreedyBounded(); if (madeProgress) { // Convert buffered messages into a filled join if each target has at least one buffered message lock (IncomingLock) { if (AllTargetsHaveAtLeastOneMessage) { _joinFilledAction(); // Pluck a message from each target _joinsCreated++; // If we are in non-greedy mode, do this once per join if (!_dataflowBlockOptions.Greedy && _boundingState != null) _boundingState.CurrentCount += 1; } } } timesThroughLoop++; } while (madeProgress && timesThroughLoop < maxMessagesPerTask); } catch (Exception exception) { // We can trigger completion of the JoinBlock by completing one target. // It doesn't matter which one. So we always complete the first one. Debug.Assert(_targets.Length > 0, "A join must have targets."); _targets[0].CompleteCore(exception, dropPendingMessages: true, releaseReservedMessages: true); // The finally section will do the block completion. } finally { lock (IncomingLock) { // We're no longer processing, so null out the processing task _taskForInputProcessing = null; // However, we may have given up early because we hit our own configured // processing limits rather than because we ran out of work to do. If that's // the case, make sure we spin up another task to keep going. ProcessAsyncIfNecessary(isReplacementReplica: true); // If, however, we stopped because we ran out of work to do and we // know we'll never get more, then complete. CompleteBlockIfPossible(); } } } /// Notifies the block that one or more items was removed from the queue. /// The number of items removed. internal void OnItemsRemoved(int numItemsRemoved) { Contract.Requires(numItemsRemoved > 0, "Number of items removed needs to be positive."); Common.ContractAssertMonitorStatus(IncomingLock, held: false); // If we're bounding, we need to know when an item is removed so that we // can update the count that's mirroring the actual count in the source's queue, // and potentially kick off processing to start consuming postponed messages. if (_boundingState != null) { lock (IncomingLock) { // Decrement the count, which mirrors the count in the source half Debug.Assert(_boundingState.CurrentCount - numItemsRemoved >= 0, "It should be impossible to have a negative number of items."); _boundingState.CurrentCount -= numItemsRemoved; ProcessAsyncIfNecessary(); CompleteBlockIfPossible(); } } } /// Gets the object to display in the debugger display attribute. [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] [SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider")] private object DebuggerDisplayContent { get { var displayJoin = _ownerJoin as IDebuggerDisplay; return string.Format("Block=\"{0}\"", displayJoin != null ? displayJoin.Content : _ownerJoin); } } } }