-
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
round robin polling for assigned partitions #63
Conversation
Initially in my Consumer::poll() method I was thinking to poll all queues until I get a valid message (see below) but the issue with this is the blocking call on
A similar problem happens in the batch poll case. How do I split batches between N partitions? Do I get Thoughts on this? |
2e46e87
to
4c7c98e
Compare
This final version works well. I fixed all the issues related to rd_kafka_poll not working. |
c729a1d
to
5a35ff4
Compare
As I mentioned in #62, other users may want to consume using other strategies. Instead of tying this to the The reason why I created different utility classes (e.g. |
Ok I can easily do that. I'll expose a If you can review the rest of the code (minus the Consumer class changes) that would be a good start. |
Would you lean more towards inheritance, i.e. some modified consumer derived class which hides the poll methods of the parent class or a helper class where you pass-in a consumer object and then it queries queues on it and polls them? I would lean more towards the latter |
Or thirdly, you could inject a polling strategy object into the consumer which would alter its runtime poll functions |
I meant just like the |
Ok thanks, will submit final PR soon. Btw, the |
5a35ff4
to
0b686a3
Compare
@mfontanini Code complete, please review. Will fix the failing assertion... |
5a661dd
to
f2a5cb9
Compare
Please don't use |
Hi, yes will do. I'm actually making one more change, so stay tuned. |
Ok done. I missed the scenario where the topic partitions list could contain multiple (different) topics during assignment/revocation. |
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.
Some comments, I'll give it another round soon.
include/cppkafka/consumer.h
Outdated
* The returned message *might* be empty. If's necessary to check that it's a valid one before | ||
* using it: | ||
* | ||
* \return A message. The returned message *might* be empty. If's necessary to check |
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.
Typo: If's
/** | ||
* Gets the configured timeout. | ||
* | ||
* \sa Queue::set_timeout |
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 seems to be set_consume_timeout
although I think I prefer set_timeout
for consistency with the rest of the code.
* \brief This adapter changes the default polling strategy of the Consumer into a fair round-robin | ||
* polling mechanism. | ||
* | ||
* The default librdkafka (and cppkafka) poll() and poll_batch() behavior is to consume batches of |
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 this comment explains way too much. I don't think there's a need to explain the internals on rdkafka here. I would just remove all of this and keep the last paragraph, the "This adapter allows fair..." one.
src/CMakeLists.txt
Outdated
utils/backoff_performer.cpp | ||
utils/backoff_committer.cpp | ||
) | ||
file(GLOB SOURCES *.cpp utils/*.cpp) |
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 prefer not using glob for source files as it can have some bad side effects. Also please keep your changes self contained: you could have just added one line here instead of removing an entire block of code and replacing it with something else.
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.
Could you be more specific as to the bad side effects? I've been using this as long as I can remember and never had problems. Also we're using it for generating the global header file which is why I wanted to add it here. I also don't like having to edit the CMakeLists.txt
file every time a cpp file gets added.
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://stackoverflow.com/questions/32411963/why-is-cmake-file-glob-evil. This is not a massive project with hundreds of files and it's not like new files get added every day anyway.
return messages; | ||
} | ||
// concatenate both lists | ||
messages.reserve(messages.size() + partition_messages.size()); |
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.
There's no need to reserve here.
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 use reserve for performance reasons and I think we should keep it as the batch size could be quite large. That's the reason I added the return line above the reserve, in case it's empty there's not need to go forward.
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.
Insert already expands the vector (once) so this call is useless. insert
knows how many elements there are between the two you provide so it knows how much to enlarge the vector.
qlist& ref() { return queues_; } | ||
// typedefs | ||
using toppar_t = std::pair<std::string, int>; //<topic, partition> | ||
using qmap_t = std::map<toppar_t, Queue>; |
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 would call this QueueMap
, the name is short enough that it's not worth abbreviating.
using qmap_t = std::map<toppar_t, Queue>; | ||
using qiter_t = qmap_t::iterator; | ||
|
||
qmap_t& ref() { |
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 would call this get_queues
or something like that. Again, no need for extreme abbreviations for names this short.
qmap_t& ref() { | ||
return queues_; | ||
} | ||
|
||
Queue& next() { |
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.
get_next_queue
here maybe?
on_rebalance_error(error); | ||
}); | ||
// make sure we don't have any active subscriptions | ||
if (!consumer_.get_subscription().empty()) { |
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 should be checked before setting all the callback, otherwise you'll leave the Consumer
dirty.
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 changed the logic...instead of rejecting, I get current assignments which is the desired 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.
I don't understand. Are you referring to the current code or some uncommitted fix?
if (messages.empty()) { | ||
return partition_messages; | ||
} | ||
if (partition_messages.empty()) { |
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 can be removed. If partition_messages
is empty, the lines below won't do anything anyway.
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 was added to prevent the reserve()
. Ideally MessageList would not be a vector but a list, as vectors don't scale well for large number of elements as mem allocations need to be contiguous so new() can fail.
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 my comment below.
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 fixed.
So I removed that non-owning builder when creating a queue and tested with and without. The queues are still there, however if I create a second adapter (after the first one is destroyed), even though the queue handles are the same (now I'm decrementing the ref count - i.e. they are "owning") I don't get any messages whatsoever, both with the batch and non-batch versions. It's quite strange... |
There should have been a test for Also, there should be some tests for this so we can guarantee it works. You can have a look at how the travis build sets up kafka to make tests work. There basically need to be 2 topics (cppkafka_test1 and cppkafka_test2) with 3 partitions each, and when you invoke cmake you should pass |
src/consumer.cpp
Outdated
output.emplace_back(ptr); | ||
} | ||
return output; | ||
return MessageList(raw_messages.begin(), raw_messages.end()); |
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.
Nice, looks much better.
Will add the travis tests...stay tuned. Ref to commit() it returns an error saying nothing was committed. The code itself is a very thin wrapper on rdkafka library, there's not much to test in your library. |
Keep in mind you can run this locally by running Regarding the commit part, it could at least check if something is actually committed (see the |
My suspicions with destroying the queues after use are confirmed. librdkafka will flush and destroy all messages and the queue instance, however if you query a new reference to this queue at a later point in time, a new queue is created yet it's not functional - i.e. no messages are enqueued and no messages are fetched from the broker - same happens with the global consumer queue which stops working completely. |
Could you create a ticket on rdkafka regarding this with your findings? I'm not doubting what you did but this looks very broken on the rdkafka side and it shouldn't be happening. At some point the queues should be destroyed and I don't know what happens if you don't do so. |
See opened ticket with librdkafka. |
e034ded
to
deee472
Compare
Tests passed. This looks like the final version unless you have more comments. |
I've been quite busy recently and haven't had a chance to look at this. I'll review it in the next couple of days. |
48d93af
to
0788395
Compare
if (partition_messages.empty()) { | ||
return messages; | ||
return; | ||
} | ||
// concatenate both lists | ||
messages.reserve(messages.size() + partition_messages.size()); |
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 one's still not fixed.
MessageList& messages, | ||
ssize_t& count, | ||
milliseconds timeout) | ||
{ |
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.
My personal preference, but to be consistent with the rest of the code, this brace should be in the line above after the )
.
* | ||
* Each call to poll() will first consume from the global event queue and if there are | ||
* no pending events, will attempt to consume from all partitions until a valid message is found. | ||
* The timeout used on this call will be the one configured via RoundRobinPollStrategy::set_timeout. |
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 should say PollInterface
/** | ||
* \brief Polls for new messages | ||
* | ||
* Same as the other overload of RoundRobinPollStrategy::poll but the provided |
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 comment about PollInterface
/** | ||
* \brief Polls all assigned partitions for a batch of new messages in round-robin fashion | ||
* | ||
* Same as the other overload of RoundRobinPollStrategy::poll_batch but the provided |
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, re: PollInterface
*/ | ||
|
||
class RoundRobinPollStrategy : public PollStrategyBase | ||
{ |
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 comment regarding brace
tests/test_utils.h
Outdated
* util classes such as BasicConsumerDispatcher. | ||
*/ | ||
class PollStrategyAdapter : public Consumer | ||
{ |
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 comment regarding the brace
tests/CMakeLists.txt
Outdated
buffer_test.cpp | ||
compacted_topic_processor_test.cpp | ||
configuration_test.cpp | ||
topic_partition_list_test.cpp | ||
kafka_handle_base_test.cpp | ||
producer_test.cpp | ||
consumer_test.cpp | ||
roundrobin_poll_test.cpp |
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.
Extra spaces here as well
tests/roundrobin_poll_test.cpp
Outdated
vector<int> partition_order = make_roundrobin_partition_vector(total_messages); | ||
|
||
for (int i = 0; i < total_messages; ++i) { | ||
REQUIRE(runner.get_messages()[i].get_partition() == partition_order[i+1]); |
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 probably missing something but how does this guarantee the starting topic/partition will be what you expect? If say, you somehow took slightly longer to produce messages than you expect, you could end up having the consumer poll through the first partition, jump to the second and then get a message on that one. The tests seem to have succeeded but I'm curious about that potential race condition.
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.
Well most likely the partitions are emtpy, which means all the EOFs arrive in sequence, and then the first poll timeout is 1s which is more then enough to publish 3 messages, especially since kafka broker is on localhost. In my testing, the broker is in a remote datacenter cluster and it still works. I have slightly changed the test now, allowing for a different start partition in case the first poll (or other ones) times out.
tests/roundrobin_poll_test.cpp
Outdated
PollStrategyAdapter consumer(make_consumer_config()); | ||
TopicPartitionList partitions; | ||
for (int i = 0; i < KAFKA_NUM_PARTITIONS; partitions.emplace_back(KAFKA_TOPICS[0], i++)); | ||
consumer.assign(partitions); |
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.
Any reason why you're not using subscribe
here? That involves a slightly trickier code flow so it may be worth using it instead.
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 will add subscribe.
88be353
to
9a28610
Compare
@mfontanini any chance you can merge this? |
Seems like this needs a rebase. |
yeah because of other PRs which came after and were accepted before :). I'll fix it |
9a28610
to
b8fdb4c
Compare
b8fdb4c
to
ea9601b
Compare
Thanks! |
Created two polling strategies to the consumer class - the default one represents the current behavior. The other polling strategy (aka partition round-robin) allows for individual consumption of assigned partitions instead of the batch method (current implementation with common forward queue).
Also created a Queue helper class to better separate polling logic and to generalize the polling mechanism at the Consumer layer i.e. the Consumer now consumes from a list of queues (may contain a single queue in the common forwarding case) irrespective of the polling strategy and is as such decoupled from underlying queue logic.