[ 
https://issues.apache.org/jira/browse/GROOVY-11902?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18071012#comment-18071012
 ] 

ASF GitHub Bot commented on GROOVY-11902:
-----------------------------------------

Copilot commented on code in PR #2433:
URL: https://github.com/apache/groovy/pull/2433#discussion_r3035446410


##########
src/spec/doc/_working-with-io.adoc:
##########
@@ -88,6 +88,17 @@ makes it very easy actually:
 include::../test/gdk/WorkingWithIOSpecTest.groovy[tags=file_bytes,indent=0]
 ----
 
+Groovy also provides asynchronous variants for reading and writing files via 
the `groovy-nio` module.
+These return a `CompletableFuture`, making them easy to compose or use with 
`async`/`await`:

Review Comment:
   The documentation mentions using `CompletableFuture` with `async`/`await`, 
but there doesn't appear to be corresponding Groovy language support or 
referenced library in this guide. Consider rephrasing to focus on 
`CompletableFuture` composition (`thenApply/thenCompose`) and blocking 
(`get/join`), or explicitly reference the specific Groovy feature/library that 
provides `await` semantics if intended.
   ```suggestion
   These return a `CompletableFuture`, making them easy to compose using 
methods like `thenApply` and `thenCompose`, or to wait for using `get()` or 
`join()`:
   ```



##########
subprojects/groovy-nio/src/main/java/org/apache/groovy/nio/extensions/NioExtensions.java:
##########
@@ -2036,4 +2041,205 @@ public static <T> T withCloseable(Closeable self, 
@ClosureParams(value = SimpleT
         return IOGroovyMethods.withCloseable(self, action);
     }
 
+    // 
-------------------------------------------------------------------------
+    // Asynchronous file I/O extensions
+    // 
-------------------------------------------------------------------------
+
+    /**
+     * Asynchronously read the content of the Path and return it as a String
+     * using the platform's default charset.
+     * <p>
+     * Uses {@link AsynchronousFileChannel} internally, making the I/O
+     * non-blocking. The returned {@link CompletableFuture} completes
+     * when the entire file has been read.
+     *
+     * <pre class="groovyTestCase">
+     * import java.nio.file.Files
+     * def path = Files.createTempFile('test', '.txt')
+     * path.text = 'Hello async'
+     * assert path.textAsync.get() == 'Hello async'
+     * Files.delete(path)
+     * </pre>
+     *
+     * @param self the file whose content we want to read
+     * @return a CompletableFuture that completes with the file content as a 
String
+     * @since 6.0.0
+     */
+    public static CompletableFuture<String> getTextAsync(Path self) {
+        return getTextAsync(self, Charset.defaultCharset().name());
+    }
+
+    /**
+     * Asynchronously read the content of the Path and return it as a String
+     * using the specified charset.
+     *
+     * <pre class="groovyTestCase">
+     * import java.nio.file.Files
+     * def path = Files.createTempFile('test', '.txt')
+     * path.text = 'Hello async'
+     * assert path.getTextAsync('UTF-8').get() == 'Hello async'
+     * Files.delete(path)
+     * </pre>
+     *
+     * @param self    the file whose content we want to read
+     * @param charset the charset used to decode the file content
+     * @return a CompletableFuture that completes with the file content as a 
String
+     * @since 6.0.0
+     */
+    public static CompletableFuture<String> getTextAsync(Path self, String 
charset) {
+        return getBytesAsync(self).thenApply(bytes -> new String(bytes, 
Charset.forName(charset)));
+    }
+
+    /**
+     * Asynchronously read the content of the Path and return it as a byte 
array.
+     * <p>
+     * Uses {@link AsynchronousFileChannel} internally. The entire file content
+     * is read into a byte array.
+     *
+     * <pre class="groovyTestCase">
+     * import java.nio.file.Files
+     * def path = Files.createTempFile('test', '.txt')
+     * path.bytes = [72, 105] as byte[]
+     * assert path.bytesAsync.get() == [72, 105] as byte[]
+     * Files.delete(path)
+     * </pre>
+     *
+     * @param self the file whose content we want to read
+     * @return a CompletableFuture that completes with the file content as a 
byte array
+     * @since 6.0.0
+     */
+    public static CompletableFuture<byte[]> getBytesAsync(Path self) {
+        CompletableFuture<byte[]> result = new CompletableFuture<>();
+        try {
+            AsynchronousFileChannel channel = 
AsynchronousFileChannel.open(self, StandardOpenOption.READ);
+            long size = channel.size();
+            if (size > Integer.MAX_VALUE) {
+                channel.close();
+                result.completeExceptionally(new OutOfMemoryError("File too 
large: " + size));

Review Comment:
   Completing the future exceptionally with an `OutOfMemoryError` for "file too 
large" is problematic because `Error` is generally reserved for JVM/runtime 
failures and may be treated differently by callers/test frameworks. Prefer 
completing exceptionally with a regular exception type (e.g., 
`IllegalArgumentException` or `IOException`) and include guidance about size 
limits.
   ```suggestion
                   result.completeExceptionally(new IOException("File too large 
to read into a single byte array: " + size + " bytes (maximum supported size is 
" + Integer.MAX_VALUE + " bytes)"));
   ```



##########
subprojects/groovy-nio/src/main/java/org/apache/groovy/nio/extensions/NioExtensions.java:
##########
@@ -2036,4 +2041,205 @@ public static <T> T withCloseable(Closeable self, 
@ClosureParams(value = SimpleT
         return IOGroovyMethods.withCloseable(self, action);
     }
 
+    // 
-------------------------------------------------------------------------
+    // Asynchronous file I/O extensions
+    // 
-------------------------------------------------------------------------
+
+    /**
+     * Asynchronously read the content of the Path and return it as a String
+     * using the platform's default charset.
+     * <p>
+     * Uses {@link AsynchronousFileChannel} internally, making the I/O
+     * non-blocking. The returned {@link CompletableFuture} completes
+     * when the entire file has been read.
+     *
+     * <pre class="groovyTestCase">
+     * import java.nio.file.Files
+     * def path = Files.createTempFile('test', '.txt')
+     * path.text = 'Hello async'
+     * assert path.textAsync.get() == 'Hello async'
+     * Files.delete(path)
+     * </pre>
+     *
+     * @param self the file whose content we want to read
+     * @return a CompletableFuture that completes with the file content as a 
String
+     * @since 6.0.0
+     */
+    public static CompletableFuture<String> getTextAsync(Path self) {
+        return getTextAsync(self, Charset.defaultCharset().name());
+    }
+
+    /**
+     * Asynchronously read the content of the Path and return it as a String
+     * using the specified charset.
+     *
+     * <pre class="groovyTestCase">
+     * import java.nio.file.Files
+     * def path = Files.createTempFile('test', '.txt')
+     * path.text = 'Hello async'
+     * assert path.getTextAsync('UTF-8').get() == 'Hello async'
+     * Files.delete(path)
+     * </pre>
+     *
+     * @param self    the file whose content we want to read
+     * @param charset the charset used to decode the file content
+     * @return a CompletableFuture that completes with the file content as a 
String
+     * @since 6.0.0
+     */
+    public static CompletableFuture<String> getTextAsync(Path self, String 
charset) {
+        return getBytesAsync(self).thenApply(bytes -> new String(bytes, 
Charset.forName(charset)));
+    }
+
+    /**
+     * Asynchronously read the content of the Path and return it as a byte 
array.
+     * <p>
+     * Uses {@link AsynchronousFileChannel} internally. The entire file content
+     * is read into a byte array.
+     *
+     * <pre class="groovyTestCase">
+     * import java.nio.file.Files
+     * def path = Files.createTempFile('test', '.txt')
+     * path.bytes = [72, 105] as byte[]
+     * assert path.bytesAsync.get() == [72, 105] as byte[]
+     * Files.delete(path)
+     * </pre>
+     *
+     * @param self the file whose content we want to read
+     * @return a CompletableFuture that completes with the file content as a 
byte array
+     * @since 6.0.0
+     */
+    public static CompletableFuture<byte[]> getBytesAsync(Path self) {
+        CompletableFuture<byte[]> result = new CompletableFuture<>();
+        try {
+            AsynchronousFileChannel channel = 
AsynchronousFileChannel.open(self, StandardOpenOption.READ);
+            long size = channel.size();
+            if (size > Integer.MAX_VALUE) {
+                channel.close();
+                result.completeExceptionally(new OutOfMemoryError("File too 
large: " + size));
+                return result;
+            }
+            ByteBuffer buf = ByteBuffer.allocate((int) size);
+            channel.read(buf, 0, null, new CompletionHandler<Integer, Void>() {
+                @Override
+                public void completed(Integer bytesRead, Void attachment) {
+                    try {
+                        channel.close();
+                    } catch (IOException e) {
+                        result.completeExceptionally(e);
+                        return;
+                    }
+                    buf.flip();
+                    byte[] bytes = new byte[buf.remaining()];
+                    buf.get(bytes);
+                    result.complete(bytes);
+                }
+
+                @Override
+                public void failed(Throwable exc, Void attachment) {
+                    try {
+                        channel.close();
+                    } catch (IOException ignored) {
+                    }
+                    result.completeExceptionally(exc);
+                }
+            });
+        } catch (IOException e) {
+            result.completeExceptionally(e);
+        }
+        return result;
+    }
+
+    /**
+     * Asynchronously write the String content to the Path using the
+     * platform's default charset.
+     *
+     * <pre class="groovyTestCase">
+     * import java.nio.file.Files
+     * def path = Files.createTempFile('test', '.txt')
+     * path.writeAsync('Hello async').get()
+     * assert path.text == 'Hello async'
+     * Files.delete(path)
+     * </pre>
+     *
+     * @param self the file to write to
+     * @param text the text to write
+     * @return a CompletableFuture that completes when the write is finished
+     * @since 6.0.0
+     */
+    public static CompletableFuture<Void> writeAsync(Path self, String text) {
+        return writeAsync(self, text, Charset.defaultCharset().name());
+    }
+
+    /**
+     * Asynchronously write the String content to the Path using the
+     * specified charset.
+     *
+     * <pre class="groovyTestCase">
+     * import java.nio.file.Files
+     * def path = Files.createTempFile('test', '.txt')
+     * path.writeAsync('Hello async', 'UTF-8').get()
+     * assert path.text == 'Hello async'
+     * Files.delete(path)
+     * </pre>
+     *
+     * @param self    the file to write to
+     * @param text    the text to write
+     * @param charset the charset used to encode the text
+     * @return a CompletableFuture that completes when the write is finished
+     * @since 6.0.0
+     */
+    public static CompletableFuture<Void> writeAsync(Path self, String text, 
String charset) {
+        return writeBytesAsync(self, text.getBytes(Charset.forName(charset)));
+    }
+
+    /**
+     * Asynchronously write the byte array to the Path, creating the file
+     * if it doesn't exist and truncating it if it does.
+     *
+     * <pre class="groovyTestCase">
+     * import java.nio.file.Files
+     * def path = Files.createTempFile('test', '.txt')
+     * path.writeBytesAsync([72, 105] as byte[]).get()
+     * assert path.bytes == [72, 105] as byte[]
+     * Files.delete(path)
+     * </pre>
+     *
+     * @param self  the file to write to
+     * @param bytes the byte array to write
+     * @return a CompletableFuture that completes when the write is finished
+     * @since 6.0.0
+     */
+    public static CompletableFuture<Void> writeBytesAsync(Path self, byte[] 
bytes) {
+        CompletableFuture<Void> result = new CompletableFuture<>();
+        try {
+            AsynchronousFileChannel channel = 
AsynchronousFileChannel.open(self,
+                    StandardOpenOption.WRITE, StandardOpenOption.CREATE, 
StandardOpenOption.TRUNCATE_EXISTING);
+            ByteBuffer buf = ByteBuffer.wrap(bytes);
+            channel.write(buf, 0, null, new CompletionHandler<Integer, Void>() 
{
+                @Override
+                public void completed(Integer bytesWritten, Void attachment) {
+                    try {
+                        channel.close();
+                    } catch (IOException e) {
+                        result.completeExceptionally(e);
+                        return;
+                    }
+                    result.complete(null);
+                }
+
+                @Override
+                public void failed(Throwable exc, Void attachment) {
+                    try {
+                        channel.close();
+                    } catch (IOException ignored) {
+                    }
+                    result.completeExceptionally(exc);
+                }
+            });
+        } catch (IOException e) {
+            result.completeExceptionally(e);
+        }
+        return result;
+    }
+

Review Comment:
   `AsynchronousFileChannel.write` may write fewer bytes than requested. This 
implementation completes after the first callback regardless of whether the 
`ByteBuffer` still has remaining bytes, which can lead to partially-written 
files. Consider continuing the write from the next position until 
`buf.hasRemaining()` is false, then close the channel and complete the future.
   ```suggestion
               writeBytesAsync(channel, buf, 0, result);
           } catch (IOException e) {
               result.completeExceptionally(e);
           }
           return result;
       }
   
       private static void writeBytesAsync(AsynchronousFileChannel channel, 
ByteBuffer buf, long position, CompletableFuture<Void> result) {
           channel.write(buf, position, null, new CompletionHandler<Integer, 
Void>() {
               @Override
               public void completed(Integer bytesWritten, Void attachment) {
                   if (buf.hasRemaining()) {
                       writeBytesAsync(channel, buf, position + bytesWritten, 
result);
                       return;
                   }
                   try {
                       channel.close();
                   } catch (IOException e) {
                       result.completeExceptionally(e);
                       return;
                   }
                   result.complete(null);
               }
   
               @Override
               public void failed(Throwable exc, Void attachment) {
                   try {
                       channel.close();
                   } catch (IOException ignored) {
                   }
                   result.completeExceptionally(exc);
               }
           });
       }
   ```



##########
subprojects/groovy-nio/src/main/java/org/apache/groovy/nio/extensions/NioExtensions.java:
##########
@@ -2036,4 +2041,205 @@ public static <T> T withCloseable(Closeable self, 
@ClosureParams(value = SimpleT
         return IOGroovyMethods.withCloseable(self, action);
     }
 
+    // 
-------------------------------------------------------------------------
+    // Asynchronous file I/O extensions
+    // 
-------------------------------------------------------------------------
+
+    /**
+     * Asynchronously read the content of the Path and return it as a String
+     * using the platform's default charset.
+     * <p>
+     * Uses {@link AsynchronousFileChannel} internally, making the I/O
+     * non-blocking. The returned {@link CompletableFuture} completes
+     * when the entire file has been read.
+     *
+     * <pre class="groovyTestCase">
+     * import java.nio.file.Files
+     * def path = Files.createTempFile('test', '.txt')
+     * path.text = 'Hello async'
+     * assert path.textAsync.get() == 'Hello async'
+     * Files.delete(path)
+     * </pre>
+     *
+     * @param self the file whose content we want to read
+     * @return a CompletableFuture that completes with the file content as a 
String
+     * @since 6.0.0
+     */
+    public static CompletableFuture<String> getTextAsync(Path self) {
+        return getTextAsync(self, Charset.defaultCharset().name());
+    }
+
+    /**
+     * Asynchronously read the content of the Path and return it as a String
+     * using the specified charset.
+     *
+     * <pre class="groovyTestCase">
+     * import java.nio.file.Files
+     * def path = Files.createTempFile('test', '.txt')
+     * path.text = 'Hello async'
+     * assert path.getTextAsync('UTF-8').get() == 'Hello async'
+     * Files.delete(path)
+     * </pre>
+     *
+     * @param self    the file whose content we want to read
+     * @param charset the charset used to decode the file content
+     * @return a CompletableFuture that completes with the file content as a 
String
+     * @since 6.0.0
+     */
+    public static CompletableFuture<String> getTextAsync(Path self, String 
charset) {
+        return getBytesAsync(self).thenApply(bytes -> new String(bytes, 
Charset.forName(charset)));
+    }
+
+    /**
+     * Asynchronously read the content of the Path and return it as a byte 
array.
+     * <p>
+     * Uses {@link AsynchronousFileChannel} internally. The entire file content
+     * is read into a byte array.
+     *
+     * <pre class="groovyTestCase">
+     * import java.nio.file.Files
+     * def path = Files.createTempFile('test', '.txt')
+     * path.bytes = [72, 105] as byte[]
+     * assert path.bytesAsync.get() == [72, 105] as byte[]
+     * Files.delete(path)
+     * </pre>
+     *
+     * @param self the file whose content we want to read
+     * @return a CompletableFuture that completes with the file content as a 
byte array
+     * @since 6.0.0
+     */
+    public static CompletableFuture<byte[]> getBytesAsync(Path self) {
+        CompletableFuture<byte[]> result = new CompletableFuture<>();
+        try {
+            AsynchronousFileChannel channel = 
AsynchronousFileChannel.open(self, StandardOpenOption.READ);
+            long size = channel.size();
+            if (size > Integer.MAX_VALUE) {
+                channel.close();
+                result.completeExceptionally(new OutOfMemoryError("File too 
large: " + size));
+                return result;
+            }
+            ByteBuffer buf = ByteBuffer.allocate((int) size);
+            channel.read(buf, 0, null, new CompletionHandler<Integer, Void>() {
+                @Override
+                public void completed(Integer bytesRead, Void attachment) {
+                    try {
+                        channel.close();
+                    } catch (IOException e) {
+                        result.completeExceptionally(e);
+                        return;
+                    }
+                    buf.flip();
+                    byte[] bytes = new byte[buf.remaining()];
+                    buf.get(bytes);
+                    result.complete(bytes);

Review Comment:
   `AsynchronousFileChannel.read` is not guaranteed to fill the buffer in a 
single operation. This implementation issues only one read and then completes 
the future, which can return truncated data for larger files or under load. 
Consider looping (issue another `read` from the next position until EOF/buffer 
full) and only completing/closing once all bytes are read (handling `bytesRead 
== -1` as EOF).



##########
subprojects/groovy-nio/src/main/java/org/apache/groovy/nio/extensions/NioExtensions.java:
##########
@@ -2036,4 +2041,205 @@ public static <T> T withCloseable(Closeable self, 
@ClosureParams(value = SimpleT
         return IOGroovyMethods.withCloseable(self, action);
     }
 
+    // 
-------------------------------------------------------------------------
+    // Asynchronous file I/O extensions
+    // 
-------------------------------------------------------------------------
+
+    /**
+     * Asynchronously read the content of the Path and return it as a String
+     * using the platform's default charset.
+     * <p>
+     * Uses {@link AsynchronousFileChannel} internally, making the I/O
+     * non-blocking. The returned {@link CompletableFuture} completes
+     * when the entire file has been read.
+     *
+     * <pre class="groovyTestCase">
+     * import java.nio.file.Files
+     * def path = Files.createTempFile('test', '.txt')
+     * path.text = 'Hello async'
+     * assert path.textAsync.get() == 'Hello async'
+     * Files.delete(path)
+     * </pre>
+     *
+     * @param self the file whose content we want to read
+     * @return a CompletableFuture that completes with the file content as a 
String
+     * @since 6.0.0
+     */
+    public static CompletableFuture<String> getTextAsync(Path self) {
+        return getTextAsync(self, Charset.defaultCharset().name());
+    }
+
+    /**
+     * Asynchronously read the content of the Path and return it as a String
+     * using the specified charset.
+     *
+     * <pre class="groovyTestCase">
+     * import java.nio.file.Files
+     * def path = Files.createTempFile('test', '.txt')
+     * path.text = 'Hello async'
+     * assert path.getTextAsync('UTF-8').get() == 'Hello async'
+     * Files.delete(path)
+     * </pre>
+     *
+     * @param self    the file whose content we want to read
+     * @param charset the charset used to decode the file content
+     * @return a CompletableFuture that completes with the file content as a 
String
+     * @since 6.0.0
+     */
+    public static CompletableFuture<String> getTextAsync(Path self, String 
charset) {
+        return getBytesAsync(self).thenApply(bytes -> new String(bytes, 
Charset.forName(charset)));
+    }
+
+    /**
+     * Asynchronously read the content of the Path and return it as a byte 
array.
+     * <p>
+     * Uses {@link AsynchronousFileChannel} internally. The entire file content
+     * is read into a byte array.
+     *
+     * <pre class="groovyTestCase">
+     * import java.nio.file.Files
+     * def path = Files.createTempFile('test', '.txt')
+     * path.bytes = [72, 105] as byte[]
+     * assert path.bytesAsync.get() == [72, 105] as byte[]
+     * Files.delete(path)
+     * </pre>
+     *
+     * @param self the file whose content we want to read
+     * @return a CompletableFuture that completes with the file content as a 
byte array
+     * @since 6.0.0
+     */
+    public static CompletableFuture<byte[]> getBytesAsync(Path self) {
+        CompletableFuture<byte[]> result = new CompletableFuture<>();
+        try {
+            AsynchronousFileChannel channel = 
AsynchronousFileChannel.open(self, StandardOpenOption.READ);
+            long size = channel.size();
+            if (size > Integer.MAX_VALUE) {
+                channel.close();
+                result.completeExceptionally(new OutOfMemoryError("File too 
large: " + size));
+                return result;
+            }
+            ByteBuffer buf = ByteBuffer.allocate((int) size);
+            channel.read(buf, 0, null, new CompletionHandler<Integer, Void>() {
+                @Override
+                public void completed(Integer bytesRead, Void attachment) {
+                    try {
+                        channel.close();
+                    } catch (IOException e) {
+                        result.completeExceptionally(e);
+                        return;
+                    }
+                    buf.flip();
+                    byte[] bytes = new byte[buf.remaining()];
+                    buf.get(bytes);
+                    result.complete(bytes);
+                }
+
+                @Override
+                public void failed(Throwable exc, Void attachment) {
+                    try {
+                        channel.close();
+                    } catch (IOException ignored) {
+                    }
+                    result.completeExceptionally(exc);
+                }
+            });

Review Comment:
   If `channel.size()` throws an `IOException`, the catch block completes the 
future exceptionally but the already-opened `AsynchronousFileChannel` is never 
closed, leaking a file descriptor. Consider closing the channel in a 
`catch`/`finally` after open succeeds (or structuring the code so any failure 
before the completion handler is installed closes the channel).
   ```suggestion
               boolean readStarted = false;
               try {
                   long size = channel.size();
                   if (size > Integer.MAX_VALUE) {
                       channel.close();
                       result.completeExceptionally(new OutOfMemoryError("File 
too large: " + size));
                       return result;
                   }
                   ByteBuffer buf = ByteBuffer.allocate((int) size);
                   channel.read(buf, 0, null, new CompletionHandler<Integer, 
Void>() {
                       @Override
                       public void completed(Integer bytesRead, Void 
attachment) {
                           try {
                               channel.close();
                           } catch (IOException e) {
                               result.completeExceptionally(e);
                               return;
                           }
                           buf.flip();
                           byte[] bytes = new byte[buf.remaining()];
                           buf.get(bytes);
                           result.complete(bytes);
                       }
   
                       @Override
                       public void failed(Throwable exc, Void attachment) {
                           try {
                               channel.close();
                           } catch (IOException ignored) {
                           }
                           result.completeExceptionally(exc);
                       }
                   });
                   readStarted = true;
               } catch (IOException | RuntimeException e) {
                   if (!readStarted) {
                       try {
                           channel.close();
                       } catch (IOException ignored) {
                       }
                   }
                   result.completeExceptionally(e);
               }
   ```





> minor groovy-nio improvements
> -----------------------------
>
>                 Key: GROOVY-11902
>                 URL: https://issues.apache.org/jira/browse/GROOVY-11902
>             Project: Groovy
>          Issue Type: Improvement
>            Reporter: Paul King
>            Assignee: Paul King
>            Priority: Major
>
> There are some small async-friendly methods we could add to groovy-nio:
> getTextAsync, getTextAsync(charset), getBytesAsync, writeAsync, 
> writeAsync(charset), writeBytesAsync



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to