-
Notifications
You must be signed in to change notification settings - Fork 10.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement minimal implementation of HybridCache #55147
Conversation
Co-authored-by: Andrew Casey <amcasey@users.noreply.github.com>
Co-authored-by: Brennan <brecon@microsoft.com>
Co-authored-by: Brennan <brecon@microsoft.com>
Co-authored-by: Brennan <brecon@microsoft.com>
…into marc/hybrid-api
…teT.cs Co-authored-by: Safia Abdalla <safia@microsoft.com>
…netcore into marc/hybrid-api-basic
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is where I ran out of time for today. Mostly simple questions.
src/Caching/Hybrid/src/Internal/DefaultHybridCache.StampedeStateT.cs
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Responses to responses.
} | ||
return value; | ||
|
||
static void Throw() => throw new ObjectDisposedException("The cache item has been recycled before the value was obtained"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason the JIT doesn't inline methods with throws is that it wants to keep them on the stack in the event of an exception?
|
||
partial class DefaultHybridCache | ||
{ | ||
internal readonly struct StampedeKey : IEquatable<StampedeKey> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ToString seems adequate to me.
|
||
// otherwise, either nothing existed - or the thing that already exists can't be joined; | ||
// in that case, go ahead and use the state that we invented a moment ago (outside of the lock) | ||
_currentOperations[stampedeKey] = stampedeState; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there is technically still a race here. Up to you to decide if it's worth fixing though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The only competition scenario involves the remove loop, but that would mean we also couldn't join with the thing being removed (or we would have exited 5 lines above), which means we're still doing the right thing by running our own, even if we end up being the only caller. I concede there may be some extreme edge case where we end up unnecessarily having a worker, but: our aim here is to reduce that to almost zero; at that point, we're good, IMO
{ | ||
_key = key; | ||
_flags = flags; | ||
_hashCode = key.GetHashCode() ^ (int)flags; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I pointed to the apisof page because it tells you what frameworks the API exists on and what package you need to use on what framework.
TLDR; there is a package with the API for netfx.
@@ -21,17 +21,21 @@ internal sealed class DefaultJsonSerializer<T> : IHybridCacheSerializer<T> | |||
T IHybridCacheSerializer<T>.Deserialize(ReadOnlySequence<byte> source) | |||
{ | |||
var reader = new Utf8JsonReader(source); | |||
#pragma warning disable IDE0079 // unnecessary suppression: TFM-dependent | |||
#pragma warning disable IL2026, IL3050 // AOT bits |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this something that will be fixed in future previews?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe so, yes
// give it a moment for the eviction callback to kick in | ||
for (var i = 0; i < 10 && cacheItem.NeedsEvictionCallback; i++) | ||
{ | ||
await Task.Delay(250); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I strongly dislike test code like this (even though I occasionally do it too), any way to make it deterministic instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're at the mercy of MemoryCache's timing on calling the eviction callback, which seems to be non-deterministic - I'm guessing they'r using QUWI. We could make it deterministic by adding an event or something that lets us know when that happens, but that is bending the design a bit far for testing. We could perhaps add a DEBUG-only event that we hook in DEBUG?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand all of it, but I also don't see anything that makes me think we shouldn't merge it.
Ah, I missed that this was auto-merge. Well, I stand by my claim that it was mergeable, but I think a follow-up changing addressing the remaining feedback is in order. |
adding comment |
I can't speak for the "why", but: it does reduce the inlineable method size to the point where the main thing (the non-exceptional case) can often be inlined; I believe modern JIT can also see methods that always / only throw, and never try inlining, which again: can leave the non-exceptional cases more inlineable; we don't really care about the stack frames - it is more about body size, IIRC |
@BrennanConroy this really doesn't warrant an extra package; heck, we could probably remove the flags part of the hash and just use the key's hashcode, and we'd be absolutely fine |
@BrennanConroy race condition concerns addressed here: eeaf857 |
Implement minimal implementation of
HybridCache
This is the first "make something that works" iteration of
HybridCache
, that builds on theabstract
API proposed in #55084 (epic: #53255 also: #54647); it will be re-targeted atmain
as soon as #55084 is mergedBroad design overview in src/Caching/Hybrid/src/Internal/readme.md
Implemented this wave:
byte[]
) and "buffer" (ReadOnlySequence<byte>
,IBufferWriter<byte>
) L2 backendsIDistributedCache
vsIBufferDistributedCache
IBufferDistributedCache
(and we found a SqlClient glitch: Inconsistent handling of empty BLOB slices as parameters SqlClient#2465)Explicitly not implemented yet:
Here's the basic headline numbers comparing
HybridCache
to a classIDistributedCache
"get, test, deserialize | fetch+serialize+set" loop using a local Redis backend and a simple POCO type (the fetch is artificial, but the test is fundamentally a 100% hit scenario):(yes, callers could add L1 support, but many don't because it is awkward; stampede support is very tricky to get right, so is almost never implemented; the point of
HybridCache
is to make it easy to get all the right behaviors)