BitFaster.Caching icon indicating copy to clipboard operation
BitFaster.Caching copied to clipboard

Allow Scoped values that do not implement IDisposable

Open PrimaForge opened this issue 4 months ago • 1 comments

I would like to use the ScopedCache as a "While Scoped values are in use it will not be removed from the cache. The value does not have to be IDisposable." Is there a reason for the IDisposable restraint?

I would assume the value could be checked to see if it implements IDisposable and dispose the value if necessary. Of Course, I could create a Disposable wrapper, but Lifetime is already a wrapper I Would prefer not to create yet another wrapper if the constraint is no necessary.

PrimaForge avatar Sep 30 '25 19:09 PrimaForge

Scoped values are immediately removed from the cache by the eviction policy or by calling TryRemove - scoping does not pin objects in the cache or affect cache eviction. Scoping controls when Dispose is called on the cached value. It does not control when the value is evicted. In other words, using scoped with non-IDisposable objects would have no effect - the scoping mechanism exists purely to call Dispose, if there is no Dispose method scoping becomes a nop.

This is the sequence diagram from the wiki page for IDisposable and Scoped values, notice that in the middle at step 11, the object is immediately removed from the cache when TryRemove is called:

sequenceDiagram
    autonumber
    participant Thread A
    participant Thread B
    participant Thread C
    participant Cache
    participant Scope
    participant Lifetime Cache    
    participant Lifetime A
    participant Lifetime B
    Thread A->>Cache: A calls ScopedGetOrAdd
    Cache->>Scope: create scope
    activate Scope 
    Scope-->>Object: create object
    activate Object
    Cache-->Lifetime Cache: cache holds lifetime
    activate Lifetime Cache
    Cache-->Lifetime A: creates A's lifetime
    activate Lifetime A
    Lifetime A-->> Thread A: A holds lifetime
    Thread B->>Cache: B calls ScopedGetOrAdd
    Cache-->Lifetime B: creates B's lifetime
    activate Lifetime B
    Lifetime B-->> Thread B: B holds lifetime
    Thread C->>Cache: C calls TryRemove
    Cache--xLifetime Cache: Cache removes the object and disposes lifetime
    deactivate Lifetime Cache
    Thread A->>Object: Thread A uses the object
    Lifetime A->>Thread A: A disposes lifetime
    Lifetime A--x Scope: lifetime de-refs scope
    deactivate Lifetime A
    Thread B->>Object: Thread B uses the object
    Lifetime B->>Thread B: B disposes lifetime
    Lifetime B--x Scope: B de-refs scope
    deactivate Lifetime B
    Scope--xObject: dispose object
    deactivate Object
    deactivate Scope

Is there a reason for the IDisposable restraint?

Scoped caches exist purely to prevent the cache calling dispose on the cached value while other callers are still using the object after retrieving from the cache. The design rationale for scoped assumes that the objects stored in the cache are in one of two categories:

  1. Cached object is always in a valid state and cannot be marked as invalid. The common case is that cached objects are immutable and are valid for use even after the cache has evicted them. In other words, if an object is always in a valid state, it is transparent to the user of the object, having retrieved it from the cache, whether it is still cache resident while they are using it- it simply doesn't matter, because cache eviction does not mutate the object.
  2. Cached object uses native or pooled resources, or some other category of behavior that requires cleanup. After cleanup, the object is no longer valid for use by any caller. In .NET, this is typically implemented via the IDisposable contract, where calling Dispose releases resources or whatever and puts an object into an invalid state (hence ObjectDisposedException is thrown when using disposed objects).

Scoped caches solve case 2, and guarantee safety by preventing disposed objects from being returned/keeping objects alive during use/promptly cleaning up after use when all callers finish using objects. The wiki page for IDisposable and Scoped values has a code sample that illustrates a data race caused by incorrect object disposal.

bitfaster avatar Dec 04 '25 22:12 bitfaster