// 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; } }
}
}
}