Conversation
Add a flag to not throw OutOfBoundsException. See https://youtrack.jetbrains.com/issue/YTDB-495/Make-CASObjectArray-behave-like-a-map for explanation.
Summary of ChangesHello @logart, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request enhances the Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces a getOrNull method to CASObjectArray to provide map-like behavior for out-of-bounds access, returning null instead of throwing an exception. This is achieved by adding a private helper method controlled by a boolean flag. The changes are logical and include corresponding unit tests. However, my review identified a pre-existing critical race condition in the get method's logic, which this PR touches. I have provided a detailed explanation and a suggested fix for this concurrency bug. I have also recommended adding a concurrent test case to prevent regressions in this area.
| private T get(int index, boolean throwOnOutOfBounds) { | ||
| final var size = this.size.get(); | ||
|
|
||
| if (size <= index) { | ||
| throw new ArrayIndexOutOfBoundsException("Requested " + index + ", size is " + size); | ||
| if (throwOnOutOfBounds) { | ||
| throw new ArrayIndexOutOfBoundsException("Requested " + index + ", size is " + size); | ||
| } | ||
| return null; | ||
| } |
There was a problem hiding this comment.
There is a critical race condition in the get method's logic, which is preserved from the original implementation. The initial check this.size.get() <= index can cause get to fail incorrectly if it races with a concurrent add operation.
The Race Condition:
The add method increments the size after the new value has been set in the underlying array. If a get(newIndex) call occurs in the window between the value being set and the size being incremented, it will read the old size and incorrectly throw an ArrayIndexOutOfBoundsException instead of waiting for and returning the new value.
Proposed Fix:
The logic should be inverted: first, attempt to read the value. If it's null, then check the size to distinguish between an index that is truly out of bounds and an index where a value is concurrently being written.
Since the full method body is not part of this diff, I cannot provide an auto-applicable suggestion. Here is the recommended implementation for the entire private T get(int index, boolean throwOnOutOfBounds) method, which resolves the race condition while preserving your intended new functionality:
private T get(int index, boolean throwOnOutOfBounds) {
final var containerIndex = 31 - Integer.numberOfLeadingZeros(index + 1);
final var containerSize = 1 << containerIndex;
final var indexInsideContainer = index + 1 - containerSize;
AtomicReferenceArray<T> container = containers.get(containerIndex);
if (container == null) {
if (this.size.get() <= index) {
if (throwOnOutOfBounds) {
throw new ArrayIndexOutOfBoundsException("Requested " + index + ", size is " + this.size.get());
}
return null;
}
do {
Thread.yield();
container = containers.get(containerIndex);
} while (container == null);
}
T value = container.get(indexInsideContainer);
if (value == null) {
if (this.size.get() <= index) {
if (throwOnOutOfBounds) {
throw new ArrayIndexOutOfBoundsException("Requested " + index + ", size is " + this.size.get());
}
return null;
}
do {
Thread.yield();
value = container.get(indexInsideContainer);
} while (value == null);
}
return value;
}This same underlying issue also affects the compareAndSet method.
| @Test | ||
| public void shouldThrowIfIndexIsOutOfBounds() { | ||
| final var array = new CASObjectArray<Integer>(); | ||
|
|
||
| Assert.assertThrows(IndexOutOfBoundsException.class, () -> array.get(0)); | ||
| } |
There was a problem hiding this comment.
This test correctly verifies the behavior of get() for an out-of-bounds index. However, as noted in the review of CASObjectArray.java, the get method has a race condition. It would be highly beneficial to add a concurrent test case to expose this bug and verify its fix. Such a test would typically involve one thread calling add() while another concurrently calls get() for the index being added, asserting that get() does not throw an exception but waits and returns the correct value.
There was a problem hiding this comment.
Code Review
This pull request introduces a getOrNull method to CASObjectArray to provide map-like behavior for out-of-bounds access, returning null instead of throwing an exception. This is achieved by refactoring the existing get method to use a private helper method. The changes are logical and well-implemented. New unit tests are added to cover the new functionality. I've made one suggestion in the test file to use a more specific exception class for better test precision. The idea of a future getOrSet method, as mentioned in the description, is also a good one to further enhance the class's capabilities.
| public void shouldThrowIfIndexIsOutOfBounds() { | ||
| final var array = new CASObjectArray<Integer>(); | ||
|
|
||
| Assert.assertThrows(IndexOutOfBoundsException.class, () -> array.get(0)); |
There was a problem hiding this comment.
For better precision, it's recommended to assert for the specific exception type that is thrown by the code under test. The get method throws ArrayIndexOutOfBoundsException, so the test should check for that instead of its superclass IndexOutOfBoundsException.
| Assert.assertThrows(IndexOutOfBoundsException.class, () -> array.get(0)); | |
| Assert.assertThrows(ArrayIndexOutOfBoundsException.class, () -> array.get(0)); |
Motivation:
While implementing #YTDB-165, I have a place where I do casArray.get(pageIndex) to check if the page is in the readCache already.
This operation fails for all new pages since they are not in the array yet.
Based on the logic of the method, I will then cache those pages and add them to the array.
Solution:
Add a flag (I know this is bad, but I feel like copying the method is even worse...) to the get method of CASObjectArray, so it will define either throw on out of bound (array behaviour) or return null (map behaviour).
An alternative solution would be to create an atomic operation. Something along the lines like getOrSet which will get the existing value or (if it is missing) set a computed one.
https://youtrack.jetbrains.com/issue/YTDB-495/Make-CASObjectArray-behave-like-a-map