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

getting rid of a Func allocation #841

Merged
merged 4 commits into from
May 20, 2020
Merged

Conversation

bollhals
Copy link
Contributor

Proposed Changes

The AsyncConsumerWorkService was allocating a new Func for each call to Schedule (Also spotted in the images of #824 due to this roslyn bug

Types of Changes

  • Bug fix (non-breaking change which fixes issue #NNNN)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause an observable behavior change in existing systems)
  • Documentation improvements (corrections, new content, etc)
  • Cosmetic change (whitespace, formatting, etc)

Checklist

  • I have read the CONTRIBUTING.md document
  • I have signed the CA (see https://cla.pivotal.io/sign/rabbitmq)
  • All tests pass locally with my changes
  • I have added tests that prove my fix is effective or that my feature works
  • I have added necessary documentation (if appropriate)
  • Any dependent changes have been merged and published in related repositories

Further Comments

I haven't verified these changes myself, as I just stumbled upon this while looking for some other answer and I quickly edited it in Github only.

@lukebakken lukebakken self-assigned this May 20, 2020
@lukebakken lukebakken added this to the 6.1.0 milestone May 20, 2020
@bollhals
Copy link
Contributor Author

You learn something new every day, so you can't field assign a lambda function that calls an instance method. :)
Moved the initialization to the new ctor and also moved the _workPool down there to keep it at one place.

@lukebakken
Copy link
Contributor

@bollhals
Copy link
Contributor Author

True, that's actually a new bug that you've fixed as well. 👍
If it is on the hotpath the question is, how "bad" is the lock for all the gets as we'd only need it really for the add case. Are there alternatives?

@bording
Copy link
Collaborator

bording commented May 20, 2020

@lukebakken Taking a quick look at this, I'm not sure that remark is really relevant here. Seems like It would only matter if we expect the same model instance to be starting work pools from different threads.

We definitely should be avoiding adding a lock in this path, but it's not clear to me that we need it.

@lukebakken
Copy link
Contributor

OK, I'll take your word for it @bording ... I just traced the code a bit and it made my eyes cross. Only connections are supposed to be thread-safe so people abusing IModel instances are warned.

@lukebakken
Copy link
Contributor

Of course, the question is why is a ConcurrentDictionary being used in the first place 🤔

@bording
Copy link
Collaborator

bording commented May 20, 2020

OK, I'll take your word for it @bording ... I just traced the code a bit and it made my eyes cross. Only connections are supposed to be thread-safe so people abusing IModel instances are warned.

This was just a quick look, so don't take what I said as gospel! I'm just trying to think of how you'd have the same model instance calling Schedule from multuple threads in a valid scenario, and I didn't immediately come up with one.

@bording
Copy link
Collaborator

bording commented May 20, 2020

Of course, the question is why is a ConcurrentDictionary being used in the first place 🤔

We do need to protect against multiple models adding work at the same time and writing to a regular dictionary from multiple threads would require a lock to avoid corrupting the collection.

@lukebakken
Copy link
Contributor

You can see why that is confusing... "No need for a lock here, but we do in this other place in the same class"

@bording
Copy link
Collaborator

bording commented May 20, 2020

It seems like we could look at just removing a shared work service as a concept and let each model have its own work pool, removing the need for a collection at all.

@bording
Copy link
Collaborator

bording commented May 20, 2020

You can see why that is confusing... "No need for a lock here, but we do in this other place in the same class"

Where is there a lock already?

@lukebakken
Copy link
Contributor

Where is there a lock already?

The use of a ConcurrentDictionary.

I'll add the per-model work pools as a future idea.

@michaelklishin
Copy link
Member

So we removed some allocations but introduced a lock. @stebet can you please run your profiling workload and share if this is a net positive change in terms of CPU usage and allocations?

@lukebakken
Copy link
Contributor

lukebakken commented May 20, 2020

So we removed some allocations but introduced a lock

The lock is already gone!

@bording
Copy link
Collaborator

bording commented May 20, 2020

The use of a ConcurrentDictionary.

Hmm, I wouldn't have classified that as a lock, but I see what you mean.

The distinction here is that we need a ConcurrentDictionary because the collection itself could be modified concurrently, where the GetOrAdd issue is about adding the same key to that collection from separate threads, and if the func being called more than once is a problem.

In this case, we'd end up creating more than one work pool, only one of which would be stored in the collection. The other one would never have work queued to it, so it doesn't seem like it could interfere in any way. It would await forever for the dequeue to return something.

@lukebakken
Copy link
Contributor

In this case, we'd end up creating more than one work pool, only one of which would be stored in the collection. The other one would never have work queued to it, so it doesn't seem like it could interfere in any way. It would await forever for the dequeue to return something.

Would the other work pool be leaked or, since there would be no reference to it, would it be cleaned up? My guess is it would be leaked, or the resources used by it leaked... emphasis on the word "guess" 🤷

@stebet
Copy link
Contributor

stebet commented May 20, 2020

Yeah, I've wondered about this code a few times. I like @bording suggestion to just have each Model keep it's own work queue. I'll run the PR through the profiling tool now though.

@stebet
Copy link
Contributor

stebet commented May 20, 2020

This does indeed work and ends up in a reduction. Good spot there @bollhals. It wasn't apparent to me where that sneaky Func<> allocation was coming from.

Before:
Before

After:
After

@stebet
Copy link
Contributor

stebet commented May 20, 2020

And on that note, I was thinking for 7.0 that it'd be a good idea to get rid of these dispatchers, and just have dedicated Channel instances on the models that an async task would simply be reading from. That way the message deliveries would always run asynchronously and there is no need for a collection to keep track of these instances.

@lukebakken
Copy link
Contributor

Thanks @bollhals and everyone else. Interesting discussion!

@lukebakken lukebakken merged commit a6bd976 into rabbitmq:master May 20, 2020
@lukebakken lukebakken added the next-gen-todo If a rewrite happens, address this issue. label May 20, 2020
@bollhals bollhals deleted the patch-1 branch May 21, 2020 14:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
next-gen-todo If a rewrite happens, address this issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants