Skip to content

KAFKA-9419: Integer Overflow Possible with CircularIterator#7950

Merged
kkonstantine merged 6 commits into
apache:trunkfrom
belugabehr:KAFKA-9419
May 6, 2020
Merged

KAFKA-9419: Integer Overflow Possible with CircularIterator#7950
kkonstantine merged 6 commits into
apache:trunkfrom
belugabehr:KAFKA-9419

Conversation

@belugabehr

@belugabehr belugabehr commented Jan 13, 2020

Copy link
Copy Markdown
Contributor

The CircularIterator class uses a wrapping index-based approach to iterate over a list. This can be a performance problem O(n^2) for a LinkedList. Also, the index counter itself is never reset, a modulo is applied to it for every list access. At some point, it may be possible that the index counter overflows to a negative value and therefore may cause a negative index read and an ArrayIndexOutOfBoundsException.

I propose changing this implementation to avoid these two scenarios. Use the Collection Iterator classes to avoid using an index counter and it avoids having to seek to the correct index every time, this avoiding the LinkedList performance issue.

I have added unit tests to validate the new implementation.

Committer Checklist (excluded from commit message)

  • Verify design and implementation
  • Verify test coverage and CI build status
  • Verify documentation (including upgrade notes)

@rondagostino rondagostino left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for the PR. Agree with the intent to eliminate index-based access (not just for the overflow problem, but also for the O(N) performance on some lists). Need to support null elements with the collection (and test it).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why change the logic since the answer is always true?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Astute observation. So, technically speaking, it is possible to not return true. Imagine the following code....

List a = new ArrayList<>(Arrays.asList("A"));
Iterator it = new CircularIterator(a);
System.out.println(it.next());
a.clear();
System.out.println(it.next());

The Iterator is re-created on the second call to next() so this is a legal option. Now, not sure it would actually happen, but it's possible.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The constructor doesn't accept an empty list, but it doesn't copy it (or, better, if we were using persistent data structures... but alas no). The class isn't thread-safe, and iterators that the code constructs and uses are potentially fail-safe or fail-fast if the collection is modified. And the remove() method of this class throws UnsupportedOperationException.

All in all, I think the best thing is to clearly document that the class does not support modifications to the passed-in collection and always return true here (technically !isEmpty() covers all cases, but since the constructor doesn't accept an empty list the answer would always be true).

Comment thread clients/src/main/java/org/apache/kafka/common/utils/CircularIterator.java Outdated
Comment thread clients/src/main/java/org/apache/kafka/common/utils/CircularIterator.java Outdated
@belugabehr

Copy link
Copy Markdown
Contributor Author

@rondagostino Changes applied. Thanks.

@rondagostino rondagostino left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think you can get rid of hasPeek and simply keep the current peek value at all times. You can create an advance() method that you call in the constructor and in next(). Then you simply return the peek value in peek() and next() (calling advance() in next() first, of course). Then all the logic is in one place. At last I think that will work. (actually, call advance() at the end of next())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should simply return true here, right? As written now it is adjusting internal state, and it is best to isolate state manipulation to as few places as possible.

@belugabehr

belugabehr commented Jan 15, 2020 via email

Copy link
Copy Markdown
Contributor Author

@rondagostino

Copy link
Copy Markdown
Contributor

I just realized that changing from an index-based approach to an iterator one changes the contract of this class -- currently it is possible to share the list and delete from it while it is being used here because there are no iterators involved. This would no longer be the case if we go to the proposed iterator-based approach. I did a quick search and found that it isn't a problem with the 2 cases where this class is currently being used (RoundRobinAssignor and EagerAsignor). Changes in the org.apache.kafka.common.utils package do not require a KIP as well, so it looks like there is no problem here. So this comment serves as documentation that we considered this issue.

@belugabehr

Copy link
Copy Markdown
Contributor Author

@rondagostino I made the requested changes. Please review.

@rondagostino rondagostino left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Implementation LGTM; 1 comment on the test.

Comment on lines +40 to +92
@Test()
public void testCycleCollection() {
final CircularIterator<String> it = new CircularIterator<>(Arrays.asList("A", "B", "C"));

assertTrue(it.hasNext());
assertEquals("A", it.next());
assertTrue(it.hasNext());
assertEquals("B", it.next());
assertTrue(it.hasNext());
assertEquals("C", it.next());
assertTrue(it.hasNext());
assertEquals("A", it.next());
assertTrue(it.hasNext());
}

@Test()
public void testPeekCollection() {
final CircularIterator<String> it = new CircularIterator<>(Arrays.asList("A", "B", "C"));

assertTrue(it.hasNext());
assertEquals("A", it.peek());
assertTrue(it.hasNext());
assertEquals("A", it.peek());
assertTrue(it.hasNext());
assertEquals("A", it.next());

assertEquals("B", it.next());
assertTrue(it.hasNext());
assertEquals("C", it.next());
assertTrue(it.hasNext());
assertEquals("A", it.next());
assertTrue(it.hasNext());
}

@Test()
public void testPeekCollectionNullValue() {
final CircularIterator<String> it = new CircularIterator<>(Arrays.asList("A", null, "C"));

assertTrue(it.hasNext());
assertEquals("A", it.peek());
assertTrue(it.hasNext());
assertEquals("A", it.next());

assertTrue(it.hasNext());
assertEquals(null, it.peek());
assertTrue(it.hasNext());
assertEquals(null, it.peek());
assertTrue(it.hasNext());
assertEquals(null, it.next());

assertTrue(it.hasNext());
assertEquals("C", it.next());
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could combine these into a single testCycleCollection() method if you put a null value in the list (e.g. "A", null, "C") and for every one of the 4 positions (0-2 and cycling back to 0) you also check the peek() value. I think it would be clearer compared to what you have currently since the last 2 methods you have now are a bit haphazard.

@belugabehr

Copy link
Copy Markdown
Contributor Author

@rondagostino I made the requested changes to the unit test. Thanks.

@rondagostino rondagostino left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM, thanks for the PR!

@belugabehr

Copy link
Copy Markdown
Contributor Author

@rondagostino Thanks for all the reviews!

@belugabehr

Copy link
Copy Markdown
Contributor Author

@rondagostino Possible to get this merged? Thanks!

@kkonstantine

Copy link
Copy Markdown
Contributor

Thanks for the PR @belugabehr and the reviews @rondagostino

I took a quick pass and the implementation looks overall. I'll take a closer look, since I believe it makes sense to backport. Because of that it's better to wait for the outstanding 2.5.0 to be released.

In the meantime, it's definitely a good idea to add a description of the change for any non-trivial PRs.

@kkonstantine

Copy link
Copy Markdown
Contributor

ok to test

@belugabehr

Copy link
Copy Markdown
Contributor Author

@kkonstantine Can you please kick of another test so we can get this wrapped up? :)

Thanks.

@kkonstantine kkonstantine left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Almost there. Added a few final minor comments.

Comment thread clients/src/main/java/org/apache/kafka/common/utils/CircularIterator.java Outdated
Comment thread clients/src/main/java/org/apache/kafka/common/utils/CircularIterator.java Outdated
Comment thread clients/src/test/java/org/apache/kafka/common/utils/CircularIteratorTest.java Outdated
@kkonstantine

Copy link
Copy Markdown
Contributor

retest this please

@belugabehr

Copy link
Copy Markdown
Contributor Author

@kkonstantine I appreciate your help getting this over the finish line

@kkonstantine

Copy link
Copy Markdown
Contributor

Definitely. It's a nice fix. Thanks for contributing it @belugabehr

@kkonstantine

Copy link
Copy Markdown
Contributor

jdk 8: stellar
jdk 11: 2 streams eos test failures: unrelated
jkd 14: 1 more unrelated failure (kafka.api.TransactionsTest.testBumpTransactionalEpoch)

Given this outcome, I'm merging.

@kkonstantine kkonstantine left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

@kkonstantine
kkonstantine merged commit 074ab2e into apache:trunk May 6, 2020
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.

3 participants