Skip to content
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

Closed
wants to merge 5 commits into from

Conversation

danielmarbach
Copy link
Contributor

@danielmarbach danielmarbach commented Jun 13, 2019

Related to Azure Service Bus

  • Properly cancels the Task.Delay if cleanup was scheduled to not leak the task for the duration of the timeout
  • Removes the .Keys access that locks the whole concurrent dictionary
  • Makes sure to remove the key only if the snapshot matches by using collection remove

@azuresdkci
Copy link
Contributor

Can one of the admins verify this patch?

@danielmarbach
Copy link
Contributor Author

Raised lock free alternative that builds on top of this one #6577

Copy link
Contributor

@AlexGhiondea AlexGhiondea left a 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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this help?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will answer later

Copy link
Contributor Author

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
Copy link
Contributor

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.

Copy link
Contributor Author

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

Copy link
Contributor Author

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();
Copy link
Contributor

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?

Copy link
Contributor Author

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);
Copy link
Contributor

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?

Copy link
Contributor Author

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);
Copy link
Contributor

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 _ = )

Copy link
Contributor Author

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);
Copy link
Contributor

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?

Copy link
Contributor Author

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);
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@danielmarbach
Copy link
Contributor Author

Replied to all the comments. Waiting for more input before I continue

@danielmarbach
Copy link
Contributor Author

Closing in favor of #6577

@danielmarbach danielmarbach deleted the expiring-set branch July 19, 2019 08:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants