dongjoon-hyun commented on code in PR #59009:
URL: https://github.com/apache/spark/pull/59009#discussion_r4108469002


##########
core/src/main/java/org/apache/spark/unsafe/map/BytesToBytesMap.java:
##########
@@ -581,18 +609,28 @@ public Location lookup(Object keyBase, long keyOffset, 
int keyLength) {
    *
    * This function always returns the same {@link Location} instance to avoid 
object allocation.
    * This function is not thread-safe.
+   *
+   * @throws IllegalStateException if the map is not ready for use
    */
   public Location lookup(Object keyBase, long keyOffset, int keyLength, int 
hash) {
     safeLookup(keyBase, keyOffset, keyLength, loc, hash);
     return loc;
   }
 
+  private void ensureReady() {

Review Comment:
   `Location.append()` is not guarded. With a `Location` obtained before a 
failed `reset()` or `free()`, `append()` only has `assert (longArray != null)`, 
so without `-ea` it acquires a new data page first and then hits an NPE at 
`longArray.get(...)`, leaving that page in `dataPages`. Since `append()` runs 
only for new keys, adding `ensureReady()` there should be cheap. Alternatively, 
documenting that a stale `Location` is unsupported would be enough.



##########
core/src/main/java/org/apache/spark/unsafe/map/BytesToBytesMap.java:
##########
@@ -1109,30 +1153,46 @@ public int getNumDataPages() {
   }
 
   /**
-   * Returns the underline long[] of longArray.
+   * Returns the underlying long array.
+   *
+   * @throws IllegalStateException if the map is not ready for use
    */
   public LongArray getArray() {
+    ensureReady();
     assert(longArray != null);
     return longArray;
   }
 
   /**
-   * Reset this map to initialized state.
+   * Resets this map to initialized state. If the replacement pointer array 
cannot be allocated,
+   * the map remains empty but cannot be used until a later call to this 
method succeeds.
+   *
+   * @throws SparkOutOfMemoryError if the replacement pointer array cannot be 
allocated
+   * @throws IllegalStateException if the map has been freed or destructive 
iteration has begun
    */
   public void reset() {
+    if (state == MapState.FREED || state == MapState.DESTRUCTIVE) {
+      throw new IllegalStateException("BytesToBytesMap cannot be reset in 
state " + state);
+    }
+    state = MapState.RESET_FAILED;
     updatePeakMemoryUsed();
+    // Put the map into its empty state before allocating so that a failed 
allocation cannot leave
+    // stale page state. A caller may retry reset(), but cannot use the map 
until reset succeeds.

Review Comment:
   This (and the javadoc) still advertises retrying `reset()`. For the only 
in-tree caller, `UnsafeKVExternalSorter`, a failed `reset()` also drops the 
sorter holding the spilled groups, so a caller that retries `reset()` and keeps 
aggregating would silently produce a result without the pre-spill groups. Could 
we add a sentence that a caller retrying `reset()` is responsible for the 
validity of any state kept outside the map (e.g., spilled data)?



##########
core/src/test/java/org/apache/spark/unsafe/map/AbstractBytesToBytesMapSuite.java:
##########
@@ -875,4 +875,178 @@ public void freeAfterFailedReset() {
     }
   }
 
+  @Test
+  public void operationsAfterFreeFail() {
+    memoryManager.limit(PAGE_SIZE_BYTES);

Review Comment:
   nit. This `memoryManager.limit(PAGE_SIZE_BYTES)` does not seem to be needed 
for this test.



##########
core/src/main/java/org/apache/spark/unsafe/map/BytesToBytesMap.java:
##########
@@ -547,17 +566,24 @@ public Location next() {
    *
    * The returned iterator is NOT thread-safe. If the map is modified while 
iterating over it,
    * the behavior of the returned iterator is undefined.
+   *
+   * @throws IllegalStateException if a reset failed, destructive iteration 
has begun, or the map
+   * has been freed
    */
   public MapIteratorWithKeyIndex iteratorWithKeyIndex() {
+    ensureReady();
     return new MapIteratorWithKeyIndex();
   }
 
   /**
    * The maximum number of allowed keys index.
    *
    * The value of allowed keys index is in the range of [0, maxNumKeysIndex - 
1].
+   *
+   * @throws IllegalStateException if the map is not ready for use

Review Comment:
   nit. The `@throws` wording is inconsistent: the iterator methods say "if a 
reset failed, destructive iteration has begun, or the map has been freed", 
while `lookup` / `safeLookup` / `getArray` / `maxNumKeysIndex` say "if the map 
is not ready for use". Could we use one form?



##########
core/src/test/java/org/apache/spark/unsafe/map/AbstractBytesToBytesMapSuite.java:
##########
@@ -875,4 +875,178 @@ public void freeAfterFailedReset() {
     }
   }
 
+  @Test
+  public void operationsAfterFreeFail() {
+    memoryManager.limit(PAGE_SIZE_BYTES);
+    BytesToBytesMap map =
+      new BytesToBytesMap(taskMemoryManager, blockManager, serializerManager, 
256, 0.5, 4000);
+    final long[] key = new long[]{1L};
+    try {
+      map.free();
+      assertThrows(IllegalStateException.class, map::reset);
+      assertThrows(
+        IllegalStateException.class,
+        () -> map.lookup(key, Platform.LONG_ARRAY_OFFSET, 8));
+      assertThrows(IllegalStateException.class, map::getArray);
+      assertThrows(IllegalStateException.class, map::maxNumKeysIndex);
+      assertEquals(0L, taskMemoryManager.getMemoryConsumptionForThisTask());
+    } finally {
+      map.free();
+    }
+  }
+
+  @Test
+  public void configuredKeyOperationsRejectLookupAfterFree() {
+    BytesToBytesMap.KeyOperationsFactory keyOperationsFactory =

Review Comment:
   nit. This anonymous `KeyOperations` duplicates the pattern of the existing 
`caseInsensitiveWithConstantHash` fixture in this suite. It may be worth 
sharing a fixture, although the existing one is test-local.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to