KAFKA-9419: Integer Overflow Possible with CircularIterator#7950
Conversation
rondagostino
left a comment
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
Why change the logic since the answer is always true?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
|
@rondagostino Changes applied. Thanks. |
There was a problem hiding this comment.
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())
There was a problem hiding this comment.
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.
|
Ok. I think the class is trivial enough that it won't throw folks for a
loop, but I can change the next() method if you so require. The
constructor will load the first value and store it in 'peek' (currently
named). At the end of each next() call, it will load the next value,
rotating Iterators if required, into 'peek'.
The state change is an artifact of my trying to gracefully handle
manipulation of the underlying collection in hasNext(). It can be reduced
to a single boolean.
Give me a couple of days. I had a marathon of Kafka PRs and am now spending
a couple of days with people, not computers. :)
…On Tue, Jan 14, 2020, 6:10 PM Ron Dagostino ***@***.***> wrote:
***@***.**** commented on this pull request.
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.
------------------------------
In
clients/src/main/java/org/apache/kafka/common/utils/CircularIterator.java
<#7950 (comment)>:
> @OverRide
public boolean hasNext() {
+ if (this.hasPeek) {
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.
—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
<#7950?email_source=notifications&email_token=AC766E5YXIN6HCQJIL3RJN3Q5ZA4ZA5CNFSM4KGI57QKYY3PNVWWK3TUL52HS4DFWFIHK3DMKJSXC5LFON2FEZLWNFSXPKTDN5WW2ZLOORPWSZGOCRYCDLY#pullrequestreview-342892975>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AC766E44S56RRXLYWWFPIA3Q5ZA4ZANCNFSM4KGI57QA>
.
|
|
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 ( |
|
@rondagostino I made the requested changes. Please review. |
rondagostino
left a comment
There was a problem hiding this comment.
Implementation LGTM; 1 comment on the test.
| @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()); | ||
| } |
There was a problem hiding this comment.
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.
|
@rondagostino I made the requested changes to the unit test. Thanks. |
rondagostino
left a comment
There was a problem hiding this comment.
LGTM, thanks for the PR!
|
@rondagostino Thanks for all the reviews! |
|
@rondagostino Possible to get this merged? Thanks! |
|
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. |
|
ok to test |
|
@kkonstantine Can you please kick of another test so we can get this wrapped up? :) Thanks. |
kkonstantine
left a comment
There was a problem hiding this comment.
Almost there. Added a few final minor comments.
|
retest this please |
|
@kkonstantine I appreciate your help getting this over the finish line |
|
Definitely. It's a nice fix. Thanks for contributing it @belugabehr |
|
jdk 8: stellar Given this outcome, I'm merging. |
The
CircularIteratorclass 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
CollectionIteratorclasses 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)