-
Notifications
You must be signed in to change notification settings - Fork 583
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
fix issue 868 #878
fix issue 868 #878
Conversation
@danielmarbach and @stebet if you have time to check this out that would be great. @stebet if you have time to run your memory allocation and other benchmarks that would be interesting as well. Thanks! |
I'll take a look at this tomorrow :) |
/* +------------+---------+----------------+---------+------------------+ | ||
* | Frame Type | Channel | Payload length | Payload | Frame End Marker | | ||
* +------------+---------+----------------+---------+------------------+ | ||
* | 1 byte | 2 bytes | 4 bytes | x bytes | 1 byte | | ||
* +------------+---------+----------------+---------+------------------+ */ |
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.
<3
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.
Took my way too long to figure this out from the code itself, thought that might help others better understand :)
I might copy some of this code for the async branch, especially the OutboundFrame riddance. |
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 low level area is not exactly my comfort zone and I'm also a bit weak from a skill level when it comes to low level optimizations like this. I hope my review comments don't look too embarrassing. Still learning and probably will for a while!
@@ -45,26 +45,28 @@ | |||
|
|||
namespace RabbitMQ.Client.Impl | |||
{ | |||
internal struct ContentHeaderPropertyReader | |||
internal ref struct ContentHeaderPropertyReader |
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 seems like
ReadBit
ReadLong
ReadLonglong
ReadLongstr
ReadShort
are no longer used. Should we ditch them? Or is it worth keeping them around just in case?
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'm fine with ditching them, I left them as they were there before.
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.
@michaelklishin @lukebakken any thoughts? Do you prefer to have them around?
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.
If code is unused and removing it does not change the API, remove it!
return result; | ||
} | ||
|
||
public byte[] ReadLongstr() | ||
{ | ||
byte[] result = WireFormatting.ReadLongstr(_memory.Slice(_memoryOffset)); | ||
_memoryOffset += 4 + result.Length; | ||
byte[] result = WireFormatting.ReadLongstr(Span); |
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.
Seems to still alloc byte[]
does it make sense to remove the ToArray()
underneath or not worth it because the method is not used anyway? See my other comment
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.
If it is not used, I rather delete it than change it. But in general we could return a Memory, but also here, it's a different contract we implicitly get by doing so.
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.
Yeah but this is a purely internal thing . Maybe let's wait until Michael and Luke chime in
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.
IMHO change internal contracts as you see fit 👍
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.
Sounds like a fairly minor implementation detail to me? I trust @bollhals' judgment on this then :)
return result; | ||
} | ||
|
||
/// <returns>A type of <seealso cref="System.Collections.Generic.IDictionary{TKey,TValue}"/>.</returns> | ||
public Dictionary<string, object> ReadTable() | ||
{ | ||
Dictionary<string, object> result = WireFormatting.ReadTable(_memory.Slice(_memoryOffset), out int bytesRead); | ||
_memoryOffset += bytesRead; | ||
Dictionary<string, object> result = WireFormatting.ReadTable(Span, out int bytesRead); |
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.
Would we want to go down the path of actually pooling header dictionaries and then return it when the command is disposed as we do for the body buffer?
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.
Possible, but risky and a "breaking change" as the consumer would be prohibited from taking any reference to these passed dictionaries. (Maybe you can open an Issue and we can discuss there about pro / cons?
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.
Yeah true. Maybe it is not worth the risk though. I'm guessing a read-only dictionary would also not really help because someone might already somewhere abusing the writable nature of the type returned. So I'm on the fence if I even should raise an issue. @michaelklishin @lukebakken thoughts?
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've thought about this pooling. Also, caching that default BasicProperties and only allocating for the fields that change from the defaults to minimize allocations taking place there. Requires some bookeeping around when the default properties change though but should be doable and yield a perf benefit.
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.
Pooling header dictionaries feels like a 7.0 thing. I think we'd want to prove that the benefits outweigh the (probably) more complicated code.
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.
Let's handle pooling in a separate PR (and yes, sounds like a 7.0 change which may or may not be worth the complexity).
@@ -44,44 +44,42 @@ | |||
|
|||
namespace RabbitMQ.Client.Impl | |||
{ | |||
struct MethodArgumentWriter | |||
internal ref struct MethodArgumentWriter |
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 we consider ditching WriteContent
?
throw new NotSupportedException("WriteContent should not be called");
took care of the feedback except the ones with open questions, let's wait for them to be answered and then clean it up. |
@lukebakken could you take a look at the questions in some of the review comments? Sidenote: Are there other operations that are unsafe to use in multithreaded environments without protection? |
From my understanding, the main reason multi-threaded usage breaks is because of incorrect frame interleaving. This means that any AMQP commands that can be multiple frames cannot tolerate having a different command's frame inserted into the multiple-frame sequence. Any change that ensures that multiple frame commands are treated as an atomic unit should go a long way to enabling multi-threaded usage of a single channel. |
There is only one such command sent by publishers: actual |
I would think there would be a way to treat those frames as a single atomic unit without there being a lock to impact perf. |
Yeas, this is actually something that was quite easy to do with Pipelines, and is part of the work i did in the async branch. |
@stebet isn't this PR also making this problem gone as is due to the nature of how the channel reader and writer interact with each other? At least that is how I understood the code as well as @bollhals comment
|
It should be. Would be interesting to try to create a massively parallel test to see if we can break it somehow. |
Yes, this pr should fix at least the frame interleaving. I wasn‘t able to make it break anymore with my local test that published in 4 threads 50k messages each. Where as before rhias change it broke down already somewhere inthe first few thousands. |
I appreciate the discussion. I've been busy dealing with customer support escalations. |
@bollhals
in a much more common case of some data to send, it will be three or more frames depending on payload size:
All problematic scenarios with publishing on a shared channel end up with frame interleaving the server parser does not expect, e.g. something like this
or this
A group of tests that shares a channel for publishing combined with other workloads would be great to have. But even if we test manually as part of QA'ing this PR, it would still be perfectly fine for now. Thank you! |
This looks very promising to me. I'll try to spend some time on this in the next few days. It would be very interesting to add a few basic integration tests that share a channel in ways that were not previously possible. But I would also be happy to proceed with merging it without such tests since the original goal was not necessarily additional concurrency hazard safety for publishers. @bollhals @danielmarbach @stebet thanks again for your substantial contributions to this client! |
I should be able to put some test(s) together, as I was doing some experimental tests anyway. |
I think we should merge this. It is a different concern and not necessary to hold this good change up |
done and tested that it used to be failing before, passing now |
Well done :) |
Thanks everyone. |
fix issue 868 (cherry picked from commit a654b1e)
Proposed Changes
Fixes the issue #868 by modifying how we send data internally.
Previously we went from Method -> Command -> OutboundFrames -> Channel -> Memory -> Socket.
Now we go Method -> Command -> Memory -> Channel -> Socket.
This change
Types of Changes
What types of changes does your code introduce to this project?
Put an
x
in the boxes that applyChecklist
CONTRIBUTING.md
document