-
Notifications
You must be signed in to change notification settings - Fork 215
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
Producer retry #78
Producer retry #78
Conversation
48b3a8e
to
9b88b4e
Compare
Just a couple of comments, I'll look at this better later.
This is not correct as I pointed out on the previous thread. If there's no delivery report callback set then there's going to be a memory leak. Moreover, you seemed concerned about performance on other PRs you made but now you're forcing at least one memory allocation for the |
9b88b4e
to
4d43d96
Compare
Hi @mfontanini, yes you're right about the dr_callback, my bad. I was too focused on the
The idea here is that if the
Good point and this is also a very easy fix. The way I think of fixing this is to have a setter in the Producer class which enables/disables |
Aren't you doing that already? You're overwriting the pointer that gets passed to Also, there's something I'm probably missing here. So as I see it the flow goes like:
|
I'd be happy to explain:
No. The original user data is placed inside the Inside the produce functions, the original user data is just placed inside the MessageInternal structure.
On the dr_callback side, inside
Then in the dr_callback_proxy, we simply unpack the MessageInternal from Message::user_data_ to
and all the
After So the whole logic is simply swap in and out of the original user_data pointer. In the case where PS: It's nice to have test cases :) |
I see, I missed where
|
I thought about this but unfortunately it's not possible. The Also, say I inject it in I don't see it as polluting the Message class, it's delivering additional out of bound data to any end application. The |
Ok, I just remembered the other problem. Say for instance that we somehow add a setter in the |
Ok, you can check my last checkin. I added logic to only conditionally create the MessageInternal data structure and I also cleaned-up the producer code a bit. The way it works is that if the producer "sees" that there is an internal data set on a Message or MessageBuilder object, it enables itself for internal data. This is done only once per producer instance. |
1fae0ae
to
75139e4
Compare
Alright, your first comment actually doesn't provide a good reason as you can always allow the buffered producer to overwrite this stuff. However, your last comment raises a good point and I see the issue now. |
Ok i think i found a way to keep changes local to BufferedProducer, MessageBuilder and Message class only IF you're ok with keeping the shared ptr inside the Message class. Without that it's impossible. That means that the Producer and free function dr_callback_proxy will remain unchanged. Lmk pls. Message and MessageBuilder changes will be almost identical to now and just a few lines of code. |
That sounds better, I'd like to have a look :) |
Ok done! Lmk what you think. This was a bit trickier to implement... |
020887a
to
62ad433
Compare
I'll check this one tomorrow, sorry for the delay! |
include/cppkafka/message_internal.h
Outdated
public: | ||
static std::unique_ptr<MessageInternal> load(const Producer& producer, Message& message); | ||
private: | ||
struct MessageInternal { |
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.
Can you make this a class with just getters for user_data_
and internal_
? I know this is only used internally but anyway, it's good to keep this encapsulated, especially given these members aren't modified after construction.
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.
ok makes sense
try { | ||
TestParameters* test_params = get_test_parameters(); | ||
if (test_params && test_params->force_produce_error_) { | ||
throw HandleException(Error(RD_KAFKA_RESP_ERR_UNKNOWN)); | ||
} | ||
produce_message(std::forward<MessageType>(message)); | ||
produce_message(std::forward<BuilderType>(builder)); |
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 wouldn't use forward
here. Otherwise unless I go into produce_message
to check what the signature is, this makes me think you may be move constructing the parameter (e.g. produce_message
taking something by value), which would make the call to callback
below bogus. I don't think you're really gaining anything by using forward
here besides maybe confuse who's reading the 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.
yep, good point
} | ||
catch (const HandleException& ex) { | ||
// If we have a flush failure callback and it returns true, we retry producing this message later | ||
CallbackInvoker<FlushFailureCallback> callback("flush failure", flush_failure_callback_, &producer_); | ||
if (!callback || callback(std::forward<MessageType>(message), ex.get_error())) { | ||
TrackerPtr tracker = std::static_pointer_cast<Tracker>(message.internal()); | ||
if (!callback || callback(std::forward<BuilderType>(builder), ex.get_error())) { |
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 here, the callback takes a const MessageBuilder&
so this forward
doesn't do anything.
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.
ok
include/cppkafka/message_internal.h
Outdated
|
||
class Message; | ||
|
||
struct Internal { |
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.
Can you use class
here? Even if they're equivalent, this is more of a class than a struct.
|
||
template <typename BufferType> | ||
void BufferedProducer<BufferType>::sync_produce(const MessageBuilder& builder) { | ||
TrackerPtr tracker = add_tracker(const_cast<MessageBuilder&>(builder)); |
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 there's a general issue now with these member functions taking const refs and using const_cast
here. What happens if you want to produce the same MessageBuilder
twice? The second time, add_tracker
will return nullptr
so you won't be using it. I'm not sure if this would break something but I think all functions that call add_tracker
should probably remove the tracker before returning (using some RAII wrapper probably). That way the MessageBuilder
s will be clean afterwards.
The way this works, you can't really call any produce/add_message overloads using the same builder in 2 threads at the same time because they'll be fighting to set the tracker, which goes against what I'd think given the functions take const refs.
As I see it, MessageBuilder::internal
is only used in BufferedProducer
(also via MessageInternalGuard
but that one's only used in that class as well). Can't you just wrap these builders in a thin template struct that contains the original builder (probably allowing to pass along reference types so you can avoid copies) along with the tracker which will be used internally? That way you won't modify the builders at all and you'll keep the same behavior.
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 is a tricky scenario with a single MessageBuilder produced multiple times. You're absolutely right that right now two threads will compete for the internal structure, but consider this: imagine you build a MessageBuilder from a Message object. The Message::internal gets transposed inside the MessageBuilder::internal, so now if you want to produce the same MessageBuilder instance multiple times you will be using the same Internal data whatever that is (Tracker in this case or other stuff). Then this is no different from producing multiple MessageBuilders (with initial empty Internal data) and eventually the first thread gets to set the Internal data which will be used again and again. So what should be the real behavior here? Also if you produce the same Message multiple times, the behavior is identical, you only use the same Internal data instance each time. Maybe the behavior should be:
- If I have a MessgeBuilder with empty internal data, then
add_tracker
should behave like RAII. - If MessageBuilder already has some internal data then
add_tracker
should be a no-op. - Also the MessageBuilder should be synchronized via a mutex (inside a shared_ptr so the class stays copyable) so that when two threads produce the same MessageBuilder, one thread will block until the other one finishes. But this will introduce a mutex inside the MessageBuilder....And then the
shared_ptr
has to be atomic and atomics are not copyable...argh...
Or
- Wrap the MessageBuilder inside a
std::pair<BuilderType, Tracker>
and convert the internal queue into a queue of these pairs? So that when I actually call produce, ifBuilderType::internal()
is set, I use that one, otherwise I usepair::second
? This way MessageBuilder stays completly untouched untilproduce_message
is called but that means changing some of the internal APIs to accept this pair structure instead of the current BuilderType.
Actually i think there's even a simpler solution...all you need to do is work with a copy of
But now I realize the MessageBuilder is non-copyable... :( because of the Buffer class. So unless you allow copies of MessageBuilder, everything else (even the RAII wrapper you suggested) will require some sort of locking of internal or user_data access at the MessageBuilder level since that object requires access by multiple threads. |
Ok I made a change...pls take a look. |
c5a0312
to
1457d6d
Compare
1457d6d
to
aee1103
Compare
aee1103
to
a4eefac
Compare
I think this looks good! I'll have a better look at it tonight but I think it's ready to be merged. |
I modified a test case to produce twice the same |
Thanks for this one, it's a very useful addition! |
Thanks for approving :), it was an interesting PR, same as the round robin one! Quite challenging in some ways. |
Description
This PR can be decomposed logically into two segments:
BufferedProducer
Scope of changes
BufferedProducer
class. Since the mechanism is generic, other classes may provide their own private data and use it internally.BufferedProducer
,Producer
,MessageBuilder
andMessage
class. Most changes are the actual retry logic insideBufferedProducer
. The other classes have minimal changes.BufferedProducer
class to allow testing of various scenarios. Currently this was not possible.BufferedProducer::sync_produce
and provided the user with a way to set the max number of message retries. In any application, retrying is an important feature, therefore this PR addresses that at a generic level, not having to be implemented by users, every time.BufferedProducer::produce
which is asynchronous, by refactoring common logic intoasync_produce
function. The originalBuffered::produce
functions still throw on error after all the retries have been exhausted, as such the behavior is unchanged.BufferedProducer
class.Private data ownership and tracking
MessageInternal
between theuser_data
pointer and the opaque pointer provided by rdkafka when producing messages. This structure has some additionalInternal
metadata (different ones can be provided for different purposes as stated above -- in this case we use aTracker
object) and is entirely managed byunique_ptr
ownership.produce(MessageBuilder)
andproduce(Message)
] create each time a newunique_ptr<MessageInternal>
structure before passing it to rdkafka. Should the produce method throw, the objects are deleted, otherwise the pointers are released.dr_callback side
, this data is loaded inside aunique_ptr<MessageInternal>
at the beginning of thedr_callback_proxy
and is deleted on exit.Tracker
objects are managed viashared_ptr
and their lifetime depends on all owners (i.e.MessageBuilder
andMessage
instances).MessageInternal
object. As such, it is impossible for any user to cause an error or to misuse the API.User data
MessageBuilder
, as well as the user data getter inMessage
class behave exactly as before. Users can pass and retrieve their own pointers transparently.Consumer
Message
object remains unchanged.Testing
MessageBuilder
and aMessage
object multiple times.