@@ -74,6 +74,43 @@ private bool TrySetCancellationOrigin(CancellationToken token)
7474 return Interlocked . CompareExchange ( ref cancellationOrigin . Item1 , inlinedToken . Item1 , comparand : null ) is null ;
7575 }
7676
77+ [ DebuggerBrowsable ( DebuggerBrowsableState . Never ) ]
78+ private object RawToken
79+ {
80+ get
81+ {
82+ // There is rare race condition that could lead to incorrect detection of the cancellation root:
83+ // the linked token gets canceled in the same time as the timeout happens for this source.
84+ // In this case, the timeout thread resumes the attached callbacks. However, the order
85+ // of these callbacks is not guaranteed. Thus, OnTimeout is not yet called, and cancellationOrigin
86+ // is still null. The resumed thread observes CancellationOrigin == Token. But then,
87+ // the linked token calls Cancel callback that sets cancellationOrigin to the real token.
88+ // In that case, CancellationOrigin != Token. It means that the consumer of CancellationOrigin
89+ // can see two different values when the property getter is called sequentially. This
90+ // is non-deterministic behavior. Currently, nothing we can do with incorrect detection
91+ // of the cancellation root due to absence of the necessary methods in CTS. But we can
92+ // achieve deterministic behavior for CancellationOrigin and IsRootCause properties:
93+ // if cancellation is requested for this source by timeout, switch cancellationOrigin to not-null
94+ // value in getter to prevent concurrent overwrite by the linked token cancellation callback.
95+ object ? tokenCopy ;
96+ if ( CanInlineToken )
97+ {
98+ tokenCopy = IsCancellationRequested
99+ ? Interlocked . CompareExchange ( ref cancellationOrigin . Item1 , this , comparand : null )
100+ : Volatile . Read ( in cancellationOrigin . Item1 ) ;
101+
102+ tokenCopy ??= this ;
103+ }
104+ else if ( ( tokenCopy = Volatile . Read ( in cancellationOrigin . Item1 ) ) is null )
105+ {
106+ object boxedToken = Token ;
107+ tokenCopy = Interlocked . CompareExchange ( ref cancellationOrigin . Item1 , boxedToken , comparand : null ) ?? boxedToken ;
108+ }
109+
110+ return tokenCopy ;
111+ }
112+ }
113+
77114 /// <summary>
78115 /// Gets the token caused cancellation.
79116 /// </summary>
@@ -82,11 +119,13 @@ private bool TrySetCancellationOrigin(CancellationToken token)
82119 /// </remarks>
83120 public CancellationToken CancellationOrigin
84121 {
85- get => new InlinedToken ( Volatile . Read ( in cancellationOrigin . Item1 ) ) is { Item1 : not null } tokenCopy
86- ? CanInlineToken
87- ? Unsafe . BitCast < InlinedToken , CancellationToken > ( tokenCopy )
88- : Unsafe . Unbox < CancellationToken > ( tokenCopy . Item1 )
89- : Token ;
122+ get
123+ {
124+ var rawToken = RawToken ;
125+ return CanInlineToken
126+ ? Unsafe . BitCast < InlinedToken , CancellationToken > ( new ( rawToken ) )
127+ : Unsafe . Unbox < CancellationToken > ( rawToken ) ;
128+ }
90129
91130 private protected set
92131 {
@@ -99,11 +138,17 @@ private protected set
99138 /// Gets a value indicating that this token source is cancelled by the timeout associated with this source,
100139 /// or by calling <see cref="CancellationTokenSource.Cancel()"/> manually.
101140 /// </summary>
102- [ DebuggerBrowsable ( DebuggerBrowsableState . Never ) ]
103- internal bool IsRootCause => CanInlineToken
104- ? Volatile . Read ( in cancellationOrigin . Item1 ) is not { } tokenCopy || ReferenceEquals ( this , tokenCopy )
105- : Token == CancellationOrigin ;
106-
141+ internal bool IsRootCause
142+ {
143+ get
144+ {
145+ var rawToken = RawToken ;
146+ return CanInlineToken
147+ ? ReferenceEquals ( rawToken , this )
148+ : Unsafe . Unbox < CancellationToken > ( rawToken ) == Token ;
149+ }
150+ }
151+
107152 // This property checks whether the reinterpret cast CancellationToken => CancellationTokenSource
108153 // is safe. If not, just box the token.
109154 internal static bool CanInlineToken => Intrinsics . AreCompatible < CancellationToken , InlinedToken > ( )
0 commit comments