Conversation
|
Hi, I may easily be missing something but don't you have to check the local cache again after you find you are the stampede that needs to do the work? There's a race between the dictionaries where someone was adding the value and removed themselves from the |
|
@AnthonyLloyd can you outline the scenario you're thinking of, that isn't guarded by the interlocked join and the double-checked locking? The only scenario I can see there is when an in-flight operations is on the cusp of terminating, which means the try-join inside the double-checked lock fails. If that is the scenario you're thinking of, we could potentially take the lock for the final decrement+remove. That is rare but theoretically non-zero - I could try to measure impact of adding a lock here (probably negligible because we try gard to minimize the locks on the competing path). Or are you thinking of another scenario? |
|
@mgravell I think it's what you describe: |
|
OK. Probably won't be ready in time for preview 4, but I'll see what the impact of preventing that scenario is. |
|
Fun fact: in an incomplete commit earlier, I actually had a partitioned sync-lock grid to avoid any remaining contention on the lock - I ditched that when the only use was the double-check, which is itself rare. If we are going to lock on the remove path, it is probably worth resurrecting that; just a simple partition just using the low 3 bits of the key's hashcode. |
…the fact that an outgoing previous operation may have added it 2. remove any reference to IDisposable; that creates semantically invalid outcomes
|
@mgravell Ah I see you are now checking L1 again. Thanks |
|
@AnthonyLloyd the problem there, though, is that I'm not very happy with running "deserialize" inside the sync-lock; I can fix that, but it involves moving a few more pieces; sigh |
|
CI failure is in |
|
The new comments look great - thanks! The new code looks non-trivial and I'll have to look at it more carefully. @BrennanConroy will probably be interested too (I think you directed his attention here in comments on the original PR). |
|
I'll look into the CI in the morning, ta |
This is supplementary post-PR (it was auto-merged) tweaks requested in #55147, mostly for @amcasey - additional comments, etc
Additional notes on 7f8fbc6 - this removes any special treatment of
IDisposable; this is because trying to consider it causes semantic problems; the caller can't know the lifetime, so if we did this, we would have a scenario where:because during the "... other stuff", the shared instance got evicted from cache and disposed; this is unpredictable and not a supportable scenario. The correct behaviours are:
using var obj = cache.GetOrCreateAsync(...), or(the latter being less "correct" than the former)
Both of these are what we already get from simply not considering
IDisposableat all; 1. is what we get if the type is considered mutable, and 2. is what we get if it is considered immutable; so: it is already in the consumer's hands.In reality, it very much isn't worth worrying to much about
IDisposablehere; the intended use-case of cache is for transient state data, typically POCOs - not connected services etc (we're not the DI layer). IMO we should not overly distort any part of the design worrying about a pathological and hypothetical use-case that is probably best avoided with the words: "don't do that".additional notes on 223f1bd - this avoids having to deserialize inside the sync-lock (which is horrible), but to do that we need a single unified ref-count rather than the two separate ref-counts we had previously (one for the stampede state, one for the cache item), so that we can speculatively reserve our value without having to deserialize it; this actually simplifies the logic, so is desirable in its own right.