66using System . Diagnostics . CodeAnalysis ;
77using System . Threading ;
88using System . Threading . Tasks ;
9+ using static Microsoft . Extensions . Caching . Hybrid . Internal . DefaultHybridCache ;
910
1011namespace Microsoft . Extensions . Caching . Hybrid . Internal ;
1112
1213internal partial class DefaultHybridCache
1314{
1415 internal sealed class StampedeState < TState , T > : StampedeState
1516 {
16- [ DoesNotReturn ]
17- private static CacheItem < T > ThrowUnexpectedCacheItem ( ) => throw new InvalidOperationException ( "Unexpected cache item" ) ;
17+ private const HybridCacheEntryFlags FlagsDisableL1AndL2 = HybridCacheEntryFlags . DisableLocalCacheWrite | HybridCacheEntryFlags . DisableDistributedCacheWrite ;
1818
1919 private readonly TaskCompletionSource < CacheItem < T > > ? _result ;
2020 private TState ? _state ;
@@ -154,6 +154,9 @@ static async Task<T> AwaitedAsync(Task<CacheItem<T>> task)
154154 => ( await task . ConfigureAwait ( false ) ) . GetReservedValue ( ) ;
155155 }
156156
157+ [ DoesNotReturn ]
158+ private static CacheItem < T > ThrowUnexpectedCacheItem ( ) => throw new InvalidOperationException ( "Unexpected cache item" ) ;
159+
157160 [ SuppressMessage ( "Resilience" , "EA0014:The async method doesn't support cancellation" , Justification = "In this case the cancellation token is provided internally via SharedToken" ) ]
158161 [ SuppressMessage ( "Design" , "CA1031:Do not catch general exception types" , Justification = "Exception is passed through to faulted task result" ) ]
159162 private async Task BackgroundFetchAsync ( )
@@ -175,29 +178,72 @@ private async Task BackgroundFetchAsync()
175178 // nothing from L2; invoke the underlying data store
176179 if ( ( Key . Flags & HybridCacheEntryFlags . DisableUnderlyingData ) == 0 )
177180 {
178- var cacheItem = SetResult ( await _underlying ! ( _state ! , SharedToken ) . ConfigureAwait ( false ) ) ;
179-
180- // note that at this point we've already released most or all of the waiting callers; everything
181- // else here is background
182-
183- // write to L2 if appropriate
184- if ( ( Key . Flags & HybridCacheEntryFlags . DisableDistributedCacheWrite ) == 0 )
181+ // invoke the callback supplied by the caller
182+ T newValue = await _underlying ! ( _state ! , SharedToken ) . ConfigureAwait ( false ) ;
183+
184+ // If we're writing this value *anywhere*, we're going to need to serialize; this is obvious
185+ // in the case of L2, but we also need it for L1, because MemoryCache might be enforcing
186+ // SizeLimit (we can't know - it is an abstraction), and for *that* we need to know the item size.
187+ // Likewise, if we're writing to a MutableCacheItem, we'll be serializing *anyway* for the payload.
188+ //
189+ // Rephrasing that: the only scenario in which we *do not* need to serialize is if:
190+ // - it is an ImmutableCacheItem
191+ // - we're writing neither to L1 nor L2
192+
193+ CacheItem cacheItem = CacheItem ;
194+ bool skipSerialize = cacheItem is ImmutableCacheItem < T > & & ( Key . Flags & FlagsDisableL1AndL2 ) == FlagsDisableL1AndL2 ;
195+
196+ if ( skipSerialize )
185197 {
186- if ( cacheItem . TryReserveBuffer ( out var buffer ) )
187- {
188- // mutable: we've already serialized it for the shared cache item
189- await Cache . SetL2Async ( Key . Key , in buffer , _options , SharedToken ) . ConfigureAwait ( false ) ;
190- _ = cacheItem . Release ( ) ; // because we reserved
191- }
192- else if ( cacheItem . TryGetValue ( out var value ) )
198+ SetImmutableResultWithoutSerialize ( newValue ) ;
199+ }
200+ else if ( cacheItem . TryReserve ( ) )
201+ {
202+ // ^^^ The first thing we need to do is make sure we're not getting into a thread race over buffer disposal.
203+ // In particular, if this cache item is somehow so short-lived that the buffers would be released *before* we're
204+ // done writing them to L2, which happens *after* we've provided the value to consumers.
205+ RecyclableArrayBufferWriter < byte > writer = RecyclableArrayBufferWriter < byte > . Create ( MaximumPayloadBytes ) ; // note this lifetime spans the SetL2Async
206+ IHybridCacheSerializer < T > serializer = Cache . GetSerializer < T > ( ) ;
207+ serializer . Serialize ( newValue , writer ) ;
208+ BufferChunk buffer = new ( writer . DetachCommitted ( out var length ) , length , returnToPool : true ) ; // remove buffer ownership from the writer
209+ writer . Dispose ( ) ; // we're done with the writer
210+
211+ // protect "buffer" (this is why we "reserved") for writing to L2 if needed; SetResultPreSerialized
212+ // *may* (depending on context) claim this buffer, in which case "bufferToRelease" gets reset, and
213+ // the final RecycleIfAppropriate() is a no-op; however, the buffer is valid in either event,
214+ // (with TryReserve above guaranteeing that we aren't in a race condition).
215+ BufferChunk bufferToRelease = buffer ;
216+
217+ // and since "bufferToRelease" is the thing that will be returned at some point, we can make it explicit
218+ // that we do not need or want "buffer" to do any recycling (they're the same memory)
219+ buffer = buffer . DoNotReturnToPool ( ) ;
220+
221+ // set the underlying result for this operation (includes L1 write if appropriate)
222+ SetResultPreSerialized ( newValue , ref bufferToRelease , serializer ) ;
223+
224+ // Note that at this point we've already released most or all of the waiting callers. Everything
225+ // from this point onwards happens in the background, from the perspective of the calling code.
226+
227+ // Write to L2 if appropriate.
228+ if ( ( Key . Flags & HybridCacheEntryFlags . DisableDistributedCacheWrite ) == 0 )
193229 {
194- // immutable: we'll need to do the serialize ourselves
195- var writer = RecyclableArrayBufferWriter < byte > . Create ( MaximumPayloadBytes ) ; // note this lifetime spans the SetL2Async
196- Cache . GetSerializer < T > ( ) . Serialize ( value , writer ) ;
197- buffer = new ( writer . GetBuffer ( out var length ) , length , returnToPool : false ) ; // writer still owns the buffer
230+ // We already have the payload serialized, so this is trivial to do.
198231 await Cache . SetL2Async ( Key . Key , in buffer , _options , SharedToken ) . ConfigureAwait ( false ) ;
199- writer . Dispose ( ) ; // recycle on success
200232 }
233+
234+ // Release our hook on the CacheItem (only really important for "mutable").
235+ _ = cacheItem . Release ( ) ;
236+
237+ // Finally, recycle whatever was left over from SetResultPreSerialized; using "bufferToRelease"
238+ // here is NOT a typo; if SetResultPreSerialized left this value alone (immutable), then
239+ // this is our recycle step; if SetResultPreSerialized transferred ownership to the (mutable)
240+ // CacheItem, then this becomes a no-op, and the buffer only gets fully recycled when the
241+ // CacheItem itself is fully clear.
242+ bufferToRelease . RecycleIfAppropriate ( ) ;
243+ }
244+ else
245+ {
246+ throw new InvalidOperationException ( "Internal HybridCache failure: unable to reserve cache item to assign result" ) ;
201247 }
202248 }
203249 else
@@ -237,13 +283,13 @@ private void SetResultAndRecycleIfAppropriate(ref BufferChunk value)
237283 // set a result from L2 cache
238284 Debug . Assert ( value . Array is not null , "expected buffer" ) ;
239285
240- var serializer = Cache . GetSerializer < T > ( ) ;
286+ IHybridCacheSerializer < T > serializer = Cache . GetSerializer < T > ( ) ;
241287 CacheItem < T > cacheItem ;
242288 switch ( CacheItem )
243289 {
244290 case ImmutableCacheItem < T > immutable :
245291 // deserialize; and store object; buffer can be recycled now
246- immutable . SetValue ( serializer . Deserialize ( new ( value . Array ! , 0 , value . Length ) ) ) ;
292+ immutable . SetValue ( serializer . Deserialize ( new ( value . Array ! , 0 , value . Length ) ) , value . Length ) ;
247293 value . RecycleIfAppropriate ( ) ;
248294 cacheItem = immutable ;
249295 break ;
@@ -261,20 +307,43 @@ private void SetResultAndRecycleIfAppropriate(ref BufferChunk value)
261307 SetResult ( cacheItem ) ;
262308 }
263309
264- private CacheItem < T > SetResult ( T value )
310+ private void SetImmutableResultWithoutSerialize ( T value )
265311 {
312+ Debug . Assert ( ( Key . Flags & FlagsDisableL1AndL2 ) == FlagsDisableL1AndL2 , "Only expected if L1+L2 disabled" ) ;
313+
266314 // set a result from a value we calculated directly
267315 CacheItem < T > cacheItem ;
268316 switch ( CacheItem )
269317 {
270318 case ImmutableCacheItem < T > immutable :
271319 // no serialize needed
272- immutable . SetValue ( value ) ;
320+ immutable . SetValue ( value , size : - 1 ) ;
273321 cacheItem = immutable ;
274322 break ;
323+ default :
324+ cacheItem = ThrowUnexpectedCacheItem ( ) ;
325+ break ;
326+ }
327+
328+ SetResult ( cacheItem ) ;
329+ }
330+
331+ private void SetResultPreSerialized ( T value , ref BufferChunk buffer , IHybridCacheSerializer < T > serializer )
332+ {
333+ // set a result from a value we calculated directly that
334+ // has ALREADY BEEN SERIALIZED (we can optionally consume this buffer)
335+ CacheItem < T > cacheItem ;
336+ switch ( CacheItem )
337+ {
338+ case ImmutableCacheItem < T > immutable :
339+ // no serialize needed
340+ immutable . SetValue ( value , size : buffer . Length ) ;
341+ cacheItem = immutable ;
342+
343+ // (but leave the buffer alone)
344+ break ;
275345 case MutableCacheItem < T > mutable :
276- // serialization happens here
277- mutable . SetValue ( value , Cache . GetSerializer < T > ( ) , MaximumPayloadBytes ) ;
346+ mutable . SetValue ( ref buffer , serializer ) ;
278347 mutable . DebugOnlyTrackBuffer ( Cache ) ;
279348 cacheItem = mutable ;
280349 break ;
@@ -284,7 +353,6 @@ private CacheItem<T> SetResult(T value)
284353 }
285354
286355 SetResult ( cacheItem ) ;
287- return cacheItem ;
288356 }
289357
290358 private void SetResult ( CacheItem < T > value )
0 commit comments