-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
Remove locks and switch to ConcurrentDictionary implementation. #15928
Conversation
…nal implementation was causing deadlocks when calling Content Delivery API in some cases.
Hi there @diger74, thank you for this contribution! 👍 While we wait for one of the Core Collaborators team to have a look at your work, we wanted to let you know about that we have a checklist for some of the things we will consider during review:
Don't worry if you got something wrong. We like to think of a pull request as the start of a conversation, we're happy to provide guidance on improving your contribution. If you realize that you might want to make some changes then you can do that by adding new commits to the branch you created for this work and pushing new commits. They should then automatically show up as updates to this pull request. Thanks, from your friendly Umbraco GitHub bot 🤖 🙂 |
Hey, is there a chance someone could look at my pull request, please? |
Hey @diger74, yes! Thanks for this, and for the AMAZING minimal reproducible example 🙌 Let me get this reviewed and tested and I'll come back to you if anything needs tweaking! |
Thanks for this @diger74, I've tested it and it fixes the problem as expected! (thanks again for the k6 script etc. really very helpful). Before I go ahead and merge, I need to have a nerdy chat with HQ about performance to make sure we're doing the best we can here as ConcurrentDictionary slows things down a bit (which is to be expected). You may have noticed that I added a couple of quick wins like swapping to GetOrAdd and Lazy initialisation - this looks to improve things a bit. |
Hi @JasonElkin, thanks for your feedback. I'm relatively new to Umbraco therefore was a bit hesitant to add anything more than just swapping dictionary implementation. Yeah, your suggestions look good, thanks for progressing! |
@@ -382,26 +349,18 @@ private class CacheValue | |||
|
|||
private class CacheValues : CacheValue | |||
{ | |||
private Dictionary<CompositeStringStringKey, CacheValue>? _values; | |||
private readonly Lazy<ConcurrentDictionary<CompositeStringStringKey, CacheValue>> _values = new(); |
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.
Lazy shouldn't be necessary here.
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.
Thanks @Nuklon, this is something I was wondering about.
Anecdotally, always creating looks to be faster, but I'm not sure of the memory implications so popped it in a Lazy (just so then we're creating the same number of dictionaries that we were before).
@@ -33,7 +33,7 @@ internal class Property : PublishedPropertyBase | |||
private object? _interValue; | |||
|
|||
// the variant source and inter values | |||
private Dictionary<CompositeStringStringKey, SourceInterValue>? _sourceValues; | |||
private readonly Lazy<ConcurrentDictionary<CompositeStringStringKey, SourceInterValue>> _sourceValues = new(); |
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.
Just create the dictionary and avoid Lazy completely?
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.
Same as above, if creating every time is more efficient overall then happy to go with that - but keen to make sure.
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.
Agree. Lazy has historically caused a lot of trouble with disposal and memory leakage.
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.
Hmm. Come to think of it, the _sourceValues
dictionary is only created for variant properties. In a LOT of cases, the property is not going to be variant, so the up-front allocation of a dictionary is an overhead compared to today.
Perhaps the Lazy
approach is justified here.
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.
Just browsing a bit through the code it seems this _sourceValues is used quite a lot (either through HasValue or GetSourceValue).
Probably not ideal to use ConcurrentDictionary here. https://frugalcafe.beehiiv.com/p/silver-bullet-concurrentdictionary. |
Thanks @nzdev, that's a really helpful article! In #5827, there's a reproducible. It's a race condition that can occur when two documents reference each other via properties. DocA -> ContentPickerProperty -> DocB If two threads access DocA and DocB simultaneously then they deadlock because each document is waiting for the other's properties to resolve. The fix makes logical sense. Umbraco needs to support the above scenario, so the property class shouldn't be locking - and this PR removes the locks. On top of this, as @diger74 points out, the locks are for the dictionary, but they also lock idempotent methods that don't touch the dictionary at all. We don't want to add unnecessary memory overhead, but we need exactly what ConcurrentDictionary does here. The dictionary will only be used for properties with variants, and it's likely to only contain a few entries - using the constructor that set's the default capacity, as pointed out in the article should help: If there's a better way of caching the |
@@ -33,7 +33,7 @@ internal class Property : PublishedPropertyBase | |||
private object? _interValue; | |||
|
|||
// the variant source and inter values | |||
private readonly Lazy<ConcurrentDictionary<CompositeStringStringKey, SourceInterValue>> _sourceValues = new(); | |||
private readonly Lazy<ConcurrentDictionary<CompositeStringStringKey, SourceInterValue>> _sourceValues = new(() => new(-1, 5)); |
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 this should be set depending on the expected number of entries, what's common?
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.
Hey @Nuklon,
This SO sums up the choice here...
If you want to
- optimize speed: take an initial capacity which is a prime about 30% bigger than the expected maximum dictionary size. This avoids resizing.
- optimize the memory footprint: take a prime which is about 30% bigger than the minimum expected size.
- a balance between speed and memory footprint: Take a number in between the two from above. But in any case, take a prime.
I'll double check the most common number, but it's _low, as it's effectively number of languages x variations so 5 seemed pretty sensible here.
# Conflicts: # src/Umbraco.PublishedCache.NuCache/Property.cs
So. V14 has been merged to the contrib branch and the contrib branch as in turn been merged to this PR. That leaves this PR in a bit of a pickle because it essentially doesn't work against V13.3 RC anymore 😢 As we want these changes in V13.3 RC, I have ported the changes to a new PR that targets V13.3 RC. See #16023 I'm going to close this one. Let's keep the dialog going on #16023. |
* Ported over #15928 changes for 13.3 RC * Use GetOrAdd() * Lock dictionary initialization --------- Co-authored-by: Jason Elkin <jasonelkin86@gmail.com>
* Updates JSON schema for Umbraco 10 with latest references for Forms and Deploy (#15918) * Ported over #15928 changes for 13.3 RC (#16023) * Ported over #15928 changes for 13.3 RC * Use GetOrAdd() * Lock dictionary initialization --------- Co-authored-by: Jason Elkin <jasonelkin86@gmail.com> * Make the API content response builder extendable (#16056) * Make the API content response builder extendable * DeliveryApiJsonTypeResolver needs to be extendable too * bump rc to regular * Bump to next minor * Add blocks in RTE telemetry (#16104) * Add blocks telemetry * Use constants and update tests * V13: Add property type information to telemetry (#16109) * Add property type counts to telemetry * Use constants and fix tests * Update description * V10: Fix for fallback file upload (#14892) (#15868) * Fix for fallback file upload (#14892) * Added check for file type * Removed unneeded null checks and fixed tabs * Cleaning * Cleanups, cleanups, and removal of unneeded null checks * Reverted removal of relationshipservice * Revert null check removals (too risky) --------- Co-authored-by: Ambert van Unen <AvanUnen@ilionx.com> Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com> (cherry picked from commit 0b5d1f8) * Fix up formatting --------- Co-authored-by: Ambert van Unen <ambertvu@gmail.com> * Implementors using Umbraco.Tests.Integration won't have to override GetLocalizedTextService (cherry picked from commit b001668) (cherry picked from commit 2bb56f1) * Fix logic for retrieving lastKnownElement (cherry picked from commit cae106b) * bump version * Bump version * Bump version * Since v13 properties can sometimes be of type IRichTextEditorIntermediateValue - this was unexpected in the XPath navigator code (#16121) * Webhook log improvements (#16200) * fix: include all headers in webhook log * feat: return webhook log status from server * feat: make webhook logs deep linkable * feat: add webhook log pagination * feat: improve webhook request/response body preview * V13: Optimize custom MVC routing (#16218) * Introduce EagerMatcherPolicy to conditionally bypass content routing * Ensure that the candidate we disable dynamic routing for is valid * Skip Umbraco endpoints * Simplify logic a bit * Move install logic to matcher * Ensure that dynamic routing is still skipped when in upgrade state * Fixup comments * Reduce nesting a bit * Don't show maintenance page when statically routed controllers are hít * Remove excess check, since installer requests are statically routed * V13: Optimize custom MVC routing (#16218) * Introduce EagerMatcherPolicy to conditionally bypass content routing * Ensure that the candidate we disable dynamic routing for is valid * Skip Umbraco endpoints * Simplify logic a bit * Move install logic to matcher * Ensure that dynamic routing is still skipped when in upgrade state * Fixup comments * Reduce nesting a bit * Don't show maintenance page when statically routed controllers are hít * Remove excess check, since installer requests are statically routed (cherry picked from commit ba9ddd1) * Property source level variation should only be applied when configured (#16270) * Property source level variation should only be applied when configured (#16270) (cherry picked from commit ab32bac) * Merge pull request from GHSA-j74q-mv2c-rxmp * Merge pull request from GHSA-j74q-mv2c-rxmp * Merge pull request from GHSA-j74q-mv2c-rxmp * Fix up after merge * Remove obselete test --------- Co-authored-by: Andy Butland <abutland73@gmail.com> Co-authored-by: Kenn Jacobsen <kja@umbraco.dk> Co-authored-by: Jason Elkin <jasonelkin86@gmail.com> Co-authored-by: Sven Geusens <sge@umbraco.dk> Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> Co-authored-by: Ambert van Unen <ambertvu@gmail.com> Co-authored-by: Lars-Erik <lars-erik@aabech.no> Co-authored-by: Joshua Daniel Pratt Nielsen <jdpnielsen@gmail.com> Co-authored-by: Bjarke Berg <mail@bergmania.dk> Co-authored-by: Sebastiaan Janssen <sebastiaan@umbraco.com> Co-authored-by: Rasmus John Pedersen <mail@rjp.dk>
Original implementation was causing deadlocks when calling Content Delivery API in some cases.
Prerequisites
This fixes #15827
Description
The original implementation of the property source values cache was based on Dictionary plus lock objects. It was there from long time ago, it was bulletproof and wasn't causing any issues. Until Content Delivery API was introduced.
Before Content Delivery API the cache was simply containing references to other objects since it was all in memory. Delivery API brought the option to expand reference properties into objects, that in turn can have their own properties. Colliding 2 requests for related items started creating a deadlock situation.
At first, I thought the locks were trying to defend nucache read-write scenario, however after the deeper investigation I realized nucache was implemented with ConcurrentDictionary already so there shouldn't be any need for extra locks.
Then I saw a simple Dictionary usage in Property class implementation and noticed only the use of this dictionary was requiring locks. When I swapped Dictionary to ConcurrentDictionary my original issue with deadlocks has gone.
I don't have a full historical knowledge of how this implementation was used and what additional side-effects could be (I'm new to Umbraco, only a few months). If you have a wider set of test tools you can run to identify this fix is safe that would be much appreciated.
And finally, if this fix is okay, is it possible to incorporate something similar for v12 as well, please? Our project has just gone live and we don't have short term plans (budget restriction) for major versions upgrade, however installing a minor version patch is a quick thing to do.
Appreciate your time and have a nice day,
Dmitry