// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // =+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ // // WriteOnceBlock.cs // // // A propagator block capable of receiving and storing only one message, ever. // // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Security; using System.Threading.Tasks.Dataflow.Internal; namespace System.Threading.Tasks.Dataflow { /// Provides a buffer for receiving and storing at most one element in a network of dataflow blocks. /// Specifies the type of the data buffered by this dataflow block. [DebuggerDisplay("{DebuggerDisplayContent,nq}")] [DebuggerTypeProxy(typeof(WriteOnceBlock<>.DebugView))] public sealed class WriteOnceBlock : IPropagatorBlock, IReceivableSourceBlock, IDebuggerDisplay { /// A registry used to store all linked targets and information about them. private readonly TargetRegistry _targetRegistry; /// The cloning function. private readonly Func _cloningFunction; /// The options used to configure this block's execution. private readonly DataflowBlockOptions _dataflowBlockOptions; /// Lazily initialized task completion source that produces the actual completion task when needed. private TaskCompletionSource _lazyCompletionTaskSource; /// Whether all future messages should be declined. private bool _decliningPermanently; /// Whether block completion is disallowed. private bool _completionReserved; /// The header of the singly-assigned value. private DataflowMessageHeader _header; /// The singly-assigned value. private T _value; /// Gets the object used as the value lock. private object ValueLock { get { return _targetRegistry; } } /// Initializes the . /// /// The function to use to clone the data when offered to other blocks. /// This may be null to indicate that no cloning need be performed. /// public WriteOnceBlock(Func cloningFunction) : this(cloningFunction, DataflowBlockOptions.Default) { } /// Initializes the with the specified . /// /// The function to use to clone the data when offered to other blocks. /// This may be null to indicate that no cloning need be performed. /// /// The options with which to configure this . /// The is null (Nothing in Visual Basic). public WriteOnceBlock(Func cloningFunction, DataflowBlockOptions dataflowBlockOptions) { // Validate arguments if (dataflowBlockOptions == null) throw new ArgumentNullException("dataflowBlockOptions"); Contract.EndContractBlock(); // Store the option _cloningFunction = cloningFunction; _dataflowBlockOptions = dataflowBlockOptions.DefaultOrClone(); // The target registry also serves as our ValueLock, // and thus must always be initialized, even if the block is pre-canceled, as // subsequent usage of the block may run through code paths that try to take this lock. _targetRegistry = new TargetRegistry(this); // If a cancelable CancellationToken has been passed in, // we need to initialize the completion task's TCS now. if (dataflowBlockOptions.CancellationToken.CanBeCanceled) { _lazyCompletionTaskSource = new TaskCompletionSource(); // If we've already had cancellation requested, do as little work as we have to // in order to be done. if (dataflowBlockOptions.CancellationToken.IsCancellationRequested) { _completionReserved = _decliningPermanently = true; // Cancel the completion task's TCS _lazyCompletionTaskSource.SetCanceled(); } else { // Handle async cancellation requests by declining on the target Common.WireCancellationToComplete( dataflowBlockOptions.CancellationToken, _lazyCompletionTaskSource.Task, state => ((WriteOnceBlock)state).Complete(), this); } } #if FEATURE_TRACING DataflowEtwProvider etwLog = DataflowEtwProvider.Log; if (etwLog.IsEnabled()) { etwLog.DataflowBlockCreated(this, dataflowBlockOptions); } #endif } /// Asynchronously completes the block on another task. /// /// This must only be called once all of the completion conditions are met. /// [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")] private void CompleteBlockAsync(IList exceptions) { Contract.Requires(_decliningPermanently, "We may get here only after we have started to decline permanently."); Contract.Requires(_completionReserved, "We may get here only after we have reserved completion."); Common.ContractAssertMonitorStatus(ValueLock, held: false); // If there is no exceptions list, we offer the message around, and then complete. // If there is an exception list, we complete without offering the message. if (exceptions == null) { // Offer the message to any linked targets and complete the block asynchronously to avoid blocking the caller var taskForOutputProcessing = new Task(state => ((WriteOnceBlock)state).OfferToTargetsAndCompleteBlock(), this, Common.GetCreationOptionsForTask()); #if FEATURE_TRACING DataflowEtwProvider etwLog = DataflowEtwProvider.Log; if (etwLog.IsEnabled()) { etwLog.TaskLaunchedForMessageHandling( this, taskForOutputProcessing, DataflowEtwProvider.TaskLaunchedReason.OfferingOutputMessages, _header.IsValid ? 1 : 0); } #endif // Start the task handling scheduling exceptions Exception exception = Common.StartTaskSafe(taskForOutputProcessing, _dataflowBlockOptions.TaskScheduler); if (exception != null) CompleteCore(exception, storeExceptionEvenIfAlreadyCompleting: true); } else { // Complete the block asynchronously to avoid blocking the caller Task.Factory.StartNew(state => { Tuple, IList> blockAndList = (Tuple, IList>)state; blockAndList.Item1.CompleteBlock(blockAndList.Item2); }, Tuple.Create(this, exceptions), CancellationToken.None, Common.GetCreationOptionsForTask(), TaskScheduler.Default); } } /// Offers the message and completes the block. /// /// This is called only once. /// private void OfferToTargetsAndCompleteBlock() { // OfferToTargets calls to potentially multiple targets, each of which // could be faulty and throw an exception. OfferToTargets creates a // list of all such exceptions and returns it. // If _value is null, OfferToTargets does nothing. List exceptions = OfferToTargets(); CompleteBlock(exceptions); } /// Completes the block. /// /// This is called only once. /// private void CompleteBlock(IList exceptions) { // Do not invoke the CompletionTaskSource property if there is a chance that _lazyCompletionTaskSource // has not been initialized yet and we may have to complete normally, because that would defeat the // sole purpose of the TCS being lazily initialized. Contract.Requires(_lazyCompletionTaskSource == null || !_lazyCompletionTaskSource.Task.IsCompleted, "The task completion source must not be completed. This must be the only thread that ever completes the block."); // Save the linked list of targets so that it could be traversed later to propagate completion TargetRegistry.LinkedTargetInfo linkedTargets = _targetRegistry.ClearEntryPoints(); // Complete the block's completion task if (exceptions != null && exceptions.Count > 0) { CompletionTaskSource.TrySetException(exceptions); } else if (_dataflowBlockOptions.CancellationToken.IsCancellationRequested) { CompletionTaskSource.TrySetCanceled(); } else { // Safely try to initialize the completion task's TCS with a cached completed TCS. // If our attempt succeeds (CompareExchange returns null), we have nothing more to do. // If the completion task's TCS was already initialized (CompareExchange returns non-null), // we have to complete that TCS instance. if (Interlocked.CompareExchange(ref _lazyCompletionTaskSource, Common.CompletedVoidResultTaskCompletionSource, null) != null) { _lazyCompletionTaskSource.TrySetResult(default(VoidResult)); } } // Now that the completion task is completed, we may propagate completion to the linked targets _targetRegistry.PropagateCompletion(linkedTargets); #if FEATURE_TRACING DataflowEtwProvider etwLog = DataflowEtwProvider.Log; if (etwLog.IsEnabled()) { etwLog.DataflowBlockCompleted(this); } #endif } /// void IDataflowBlock.Fault(Exception exception) { if (exception == null) throw new ArgumentNullException("exception"); Contract.EndContractBlock(); CompleteCore(exception, storeExceptionEvenIfAlreadyCompleting: false); } /// public void Complete() { CompleteCore(exception: null, storeExceptionEvenIfAlreadyCompleting: false); } private void CompleteCore(Exception exception, bool storeExceptionEvenIfAlreadyCompleting) { Contract.Requires(exception != null || !storeExceptionEvenIfAlreadyCompleting, "When storeExceptionEvenIfAlreadyCompleting is set to true, an exception must be provided."); Contract.EndContractBlock(); bool thisThreadReservedCompletion = false; lock (ValueLock) { // Faulting from outside is allowed until we start declining permanently if (_decliningPermanently && !storeExceptionEvenIfAlreadyCompleting) return; // Decline further messages _decliningPermanently = true; // Reserve Completion. // If storeExceptionEvenIfAlreadyCompleting is true, we are here to fault the block, // because we couldn't launch the offer-and-complete task. // We have to retry to just complete. We do that by pretending completion wasn't reserved. if (!_completionReserved || storeExceptionEvenIfAlreadyCompleting) thisThreadReservedCompletion = _completionReserved = true; } // This call caused us to start declining further messages, // there's nothing more this block needs to do... complete it if we just reserved completion. if (thisThreadReservedCompletion) { List exceptions = null; if (exception != null) { exceptions = new List(); exceptions.Add(exception); } CompleteBlockAsync(exceptions); } } /// public Boolean TryReceive(Predicate filter, out T item) { // No need to take the outgoing lock, as we don't need to synchronize with other // targets, and _value only ever goes from null to non-null, not the other way around. // If we have a value, give it up. All receives on a successfully // completed WriteOnceBlock will return true, as long as the message // passes the filter (all messages pass a null filter). if (_header.IsValid && (filter == null || filter(_value))) { item = CloneItem(_value); return true; } // Otherwise, nothing to receive else { item = default(T); return false; } } /// Boolean IReceivableSourceBlock.TryReceiveAll(out IList items) { // Try to receive the one item this block may have. // If we can, give back an array of one item. Otherwise, // give back null. T item; if (TryReceive(null, out item)) { items = new T[] { item }; return true; } else { items = null; return false; } } /// [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")] public IDisposable LinkTo(ITargetBlock target, DataflowLinkOptions linkOptions) { // Validate arguments if (target == null) throw new ArgumentNullException("target"); if (linkOptions == null) throw new ArgumentNullException("linkOptions"); Contract.EndContractBlock(); bool hasValue; bool isCompleted; lock (ValueLock) { hasValue = HasValue; isCompleted = _completionReserved; // If we haven't gotten a value yet and the block is not complete, add the target and bail if (!hasValue && !isCompleted) { _targetRegistry.Add(ref target, linkOptions); return Common.CreateUnlinker(ValueLock, _targetRegistry, target); } } // If we already have a value, send it along to the linking target if (hasValue) { bool useCloning = _cloningFunction != null; target.OfferMessage(_header, _value, this, consumeToAccept: useCloning); } // If completion propagation has been requested, do it safely. // The Completion property will ensure the lazy TCS is initialized. if (linkOptions.PropagateCompletion) Common.PropagateCompletionOnceCompleted(Completion, target); return Disposables.Nop; } /// public Task Completion { get { return CompletionTaskSource.Task; } } /// 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(); bool thisThreadReservedCompletion = false; lock (ValueLock) { // If we are declining messages, bail if (_decliningPermanently) return DataflowMessageStatus.DecliningPermanently; // Consume the message from the source if necessary. We do this while holding ValueLock to prevent multiple concurrent // offers from all succeeding. if (consumeToAccept) { bool consumed; messageValue = source.ConsumeMessage(messageHeader, this, out consumed); if (!consumed) return DataflowMessageStatus.NotAvailable; } // Update the header and the value _header = Common.SingleMessageHeader; _value = messageValue; // We got what we needed. Start declining permanently. _decliningPermanently = true; // Reserve Completion if (!_completionReserved) thisThreadReservedCompletion = _completionReserved = true; } // Since this call to OfferMessage succeeded (and only one can ever), complete the block // (but asynchronously so as not to block the Post call while offering to // targets, running synchronous continuations off of the completion task, etc.) if (thisThreadReservedCompletion) CompleteBlockAsync(exceptions: null); return DataflowMessageStatus.Accepted; } /// T ISourceBlock.ConsumeMessage(DataflowMessageHeader messageHeader, ITargetBlock target, out Boolean messageConsumed) { // Validate arguments if (!messageHeader.IsValid) throw new ArgumentException(SR.Argument_InvalidMessageHeader, "messageHeader"); if (target == null) throw new ArgumentNullException("target"); Contract.EndContractBlock(); // As long as the message being requested is the one we have, allow it to be consumed, // but make a copy using the provided cloning function. if (_header.Id == messageHeader.Id) { messageConsumed = true; return CloneItem(_value); } else { messageConsumed = false; return default(T); } } /// Boolean ISourceBlock.ReserveMessage(DataflowMessageHeader messageHeader, ITargetBlock target) { // Validate arguments if (!messageHeader.IsValid) throw new ArgumentException(SR.Argument_InvalidMessageHeader, "messageHeader"); if (target == null) throw new ArgumentNullException("target"); Contract.EndContractBlock(); // As long as the message is the one we have, it can be "reserved." // Reservations on a WriteOnceBlock are not exclusive, because // everyone who wants a copy can get one. return _header.Id == messageHeader.Id; } /// void ISourceBlock.ReleaseReservation(DataflowMessageHeader messageHeader, ITargetBlock target) { // Validate arguments if (!messageHeader.IsValid) throw new ArgumentException(SR.Argument_InvalidMessageHeader, "messageHeader"); if (target == null) throw new ArgumentNullException("target"); Contract.EndContractBlock(); // As long as the message is the one we have, everything's fine. if (_header.Id != messageHeader.Id) throw new InvalidOperationException(SR.InvalidOperation_MessageNotReservedByTarget); // In other blocks, upon release we typically re-offer the message to all linked targets. // We need to do the same thing for WriteOnceBlock, in order to account for cases where the block // may be linked to a join or similar block, such that the join could never again be satisfied // if it didn't receive another offer from this source. However, since the message is broadcast // and all targets can get a copy, we don't need to broadcast to all targets, only to // the target that released the message. Note that we don't care whether it's accepted // or not, nor do we care about any exceptions which may emerge (they should just propagate). Debug.Assert(_header.IsValid, "A valid header is required."); bool useCloning = _cloningFunction != null; target.OfferMessage(_header, _value, this, consumeToAccept: useCloning); } /// Clones the item. /// The item to clone. /// The cloned item. private T CloneItem(T item) { return _cloningFunction != null ? _cloningFunction(item) : item; } /// Offers the WriteOnceBlock's message to all targets. [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] private List OfferToTargets() { Common.ContractAssertMonitorStatus(ValueLock, held: false); // If there is a message, offer it to everyone. Return values // don't matter, because we only get one message and then complete, // and everyone who wants a copy can get a copy. List exceptions = null; if (HasValue) { TargetRegistry.LinkedTargetInfo cur = _targetRegistry.FirstTargetNode; while (cur != null) { TargetRegistry.LinkedTargetInfo next = cur.Next; ITargetBlock target = cur.Target; try { // Offer the message. If there's a cloning function, we force the target to // come back to us to consume the message, allowing us the opportunity to run // the cloning function once we know they want the data. If there is no cloning // function, there's no reason for them to call back here. bool useCloning = _cloningFunction != null; target.OfferMessage(_header, _value, this, consumeToAccept: useCloning); } catch (Exception exc) { // Track any erroneous exceptions that may occur // and return them to the caller so that they may // be logged in the completion task. Common.StoreDataflowMessageValueIntoExceptionData(exc, _value); Common.AddException(ref exceptions, exc); } cur = next; } } return exceptions; } /// Ensures the completion task's TCS is initialized. /// The completion task's TCS. private TaskCompletionSource CompletionTaskSource { get { // If the completion task's TCS has not been initialized by now, safely try to initialize it. // It is very important that once a completion task/source instance has been handed out, // it remains the block's completion task. if (_lazyCompletionTaskSource == null) { Interlocked.CompareExchange(ref _lazyCompletionTaskSource, new TaskCompletionSource(), null); } return _lazyCompletionTaskSource; } } /// Gets whether the block is storing a value. private bool HasValue { get { return _header.IsValid; } } /// Gets the value being stored by the block. private T Value { get { return _header.IsValid ? _value : default(T); } } /// public override string ToString() { return Common.GetNameForDebugger(this, _dataflowBlockOptions); } /// The data to display in the debugger display attribute. [SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider")] private object DebuggerDisplayContent { get { return string.Format("{0}, HasValue={1}, Value={2}", Common.GetNameForDebugger(this, _dataflowBlockOptions), HasValue, Value); } } /// 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 WriteOnceBlock. private sealed class DebugView { /// The WriteOnceBlock being viewed. private readonly WriteOnceBlock _writeOnceBlock; /// Initializes the debug view. /// The WriteOnceBlock to view. public DebugView(WriteOnceBlock writeOnceBlock) { Contract.Requires(writeOnceBlock != null, "Need a block with which to construct the debug view."); _writeOnceBlock = writeOnceBlock; } /// Gets whether the WriteOnceBlock has completed. public bool IsCompleted { get { return _writeOnceBlock.Completion.IsCompleted; } } /// Gets the block's Id. public int Id { get { return Common.GetBlockId(_writeOnceBlock); } } /// Gets whether the WriteOnceBlock has a value. public bool HasValue { get { return _writeOnceBlock.HasValue; } } /// Gets the WriteOnceBlock's value if it has one, or default(T) if it doesn't. public T Value { get { return _writeOnceBlock.Value; } } /// Gets the DataflowBlockOptions used to configure this block. public DataflowBlockOptions DataflowBlockOptions { get { return _writeOnceBlock._dataflowBlockOptions; } } /// Gets the set of all targets linked from this block. public TargetRegistry LinkedTargets { get { return _writeOnceBlock._targetRegistry; } } } } }