-
Notifications
You must be signed in to change notification settings - Fork 4.9k
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
Make ConcurrentExpiringSet not leak the cleanup task for the period of delayBetweenCleanups #6576
Conversation
…f delayBetweenCleanups
Can one of the admins verify this patch? |
Raised lock free alternative that builds on top of this one #6577 |
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 for the PR.
I left a couple of comments.
using System.Threading.Tasks; | ||
|
||
sealed class ConcurrentExpiringSet<TKey> | ||
{ | ||
readonly ConcurrentDictionary<TKey, DateTime> dictionary; | ||
readonly ICollection<KeyValuePair<TKey, DateTime>> dictionaryAsCollection; |
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.
How does this help?
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.
Will answer later
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.
ConcurrentDictionary implements the ICollection<KeyValuePair<TKey, TValue>>
interface implicitly. By having a reference to that we can use the Remove(KeyValuePair<TKey, TValue> kvp)
method which calls into TryRemoveInternal
with matchValue
set to true
which then makes sure that the value matches the reference we get from looping over. This is basically the more accurate implementation that avoids removing a key when the value was updated in the meantime.
readonly object cleanupSynObject = new object(); | ||
CancellationTokenSource tokenSource = new CancellationTokenSource(); // doesn't need to be disposed because it doesn't own a timer |
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 CancellationTokenSource implements IDisposable and I think it might internally use a timer.
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.
It does. As far as I remember it only allocates disposable resources like timers when used with timeouts. Happy though to add
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.
See https://referencesource.microsoft.com/#mscorlib/system/threading/CancellationTokenSource.cs,554 that's why I did not implement the dispose call because we are not using the registrations or the timer path of the disposable
{ | ||
this.tokenSource.Cancel(); | ||
this.dictionary.Clear(); | ||
this.tokenSource = new CancellationTokenSource(); |
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.
Should this be disposed of as it implements IDisposable?
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.
See comment above
@@ -40,28 +52,38 @@ void ScheduleCleanup() | |||
} | |||
|
|||
this.cleanupScheduled = true; | |||
Task.Run(async () => await this.CollectExpiredEntriesAsync().ConfigureAwait(false)); | |||
_ = this.CollectExpiredEntriesAsync(token); |
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 will start a task that will do the collection without waiting for the cleanup to happen -- is this the design?
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.
Yes see previous implementation did a fire and forget Task.Run
@@ -40,28 +52,38 @@ void ScheduleCleanup() | |||
} | |||
|
|||
this.cleanupScheduled = true; | |||
Task.Run(async () => await this.CollectExpiredEntriesAsync().ConfigureAwait(false)); | |||
_ = this.CollectExpiredEntriesAsync(token); |
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.
You don't need to assign the result of the method call if you don't plan to access it later (ie. no need for _ =
)
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.
That is technically accurate. Though I consider it best practice to explicitly ignore not consumed tasks. Because if at any time this method would return a task the compiler would start exposing warning. I have to double check the VS diagbostics if they even detect dropped task in void methods
} | ||
} | ||
|
||
this.ScheduleCleanup(); | ||
this.ScheduleCleanup(token); |
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.
What happens if the token is canceled?
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.
It will cancel everything and not throw see try catch further down. Are you hinting that you'd like an realy exit before acquiring the lock?
{ | ||
this.dictionary.TryRemove(key, out _); | ||
this.dictionaryAsCollection.Remove(kvp); |
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 this will modify the collection while you are iterating over it, won't it?
Since both dictionary
and dictionaryAsCollection
point to the same object.
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.
sdk/servicebus/Microsoft.Azure.ServiceBus/tests/Primitives/ConcurrentExpiringSetTests.cs
Show resolved
Hide resolved
Replied to all the comments. Waiting for more input before I continue |
Closing in favor of #6577 |
Related to Azure Service Bus
Task.Delay
if cleanup was scheduled to not leak the task for the duration of the timeout.Keys
access that locks the whole concurrent dictionary