diff --git a/java/client/src/main/java/glide/api/RedisClusterClient.java b/java/client/src/main/java/glide/api/RedisClusterClient.java index d839f730da..19689594e7 100644 --- a/java/client/src/main/java/glide/api/RedisClusterClient.java +++ b/java/client/src/main/java/glide/api/RedisClusterClient.java @@ -50,6 +50,7 @@ import glide.api.commands.ScriptingAndFunctionsClusterCommands; import glide.api.commands.ServerManagementClusterCommands; import glide.api.commands.TransactionsClusterCommands; +import glide.api.logging.Logger; import glide.api.models.ClusterTransaction; import glide.api.models.ClusterValue; import glide.api.models.GlideString; @@ -1011,7 +1012,7 @@ private static final class NativeClusterScanCursor private boolean isClosed = false; // This is for internal use only. - public NativeClusterScanCursor(String cursorHandle) { + public NativeClusterScanCursor(@NonNull String cursorHandle) { this.cursorHandle = cursorHandle; this.isFinished = FINISHED_CURSOR_MARKER.equals(cursorHandle); } @@ -1043,10 +1044,18 @@ protected void finalize() throws Throwable { private void internalClose() { if (!isClosed) { - ClusterScanCursorResolver.releaseNativeCursor(cursorHandle); - - // Mark the cursor as closed to avoid double-free (if close() gets called more than once). - isClosed = true; + try { + ClusterScanCursorResolver.releaseNativeCursor(cursorHandle); + } catch (Exception ex) { + Logger.log( + Logger.Level.ERROR, + "ClusterScanCursor", + () -> "Error releasing cursor " + cursorHandle + ": " + ex.getMessage()); + Logger.log(Logger.Level.ERROR, "ClusterScanCursor", ex); + } finally { + // Mark the cursor as closed to avoid double-free (if close() gets called more than once). + isClosed = true; + } } } } diff --git a/java/client/src/main/java/glide/api/commands/GenericClusterCommands.java b/java/client/src/main/java/glide/api/commands/GenericClusterCommands.java index fffa9db5b5..f5515cf76d 100644 --- a/java/client/src/main/java/glide/api/commands/GenericClusterCommands.java +++ b/java/client/src/main/java/glide/api/commands/GenericClusterCommands.java @@ -162,19 +162,31 @@ public interface GenericClusterCommands { * Using the same cursor object for multiple iterations will result in the same keys or unexpected * behavior. For more information about the Cluster Scan implementation, see Cluster - * Scan. As with the SCAN command, the method can be used to iterate over the keys in the - * database, to return all keys the database have from the time the scan started till the scan - * ends. The same key can be returned in multiple scans iteration. + * Scan. * + *

As with the SCAN command, the method can be used to iterate over the keys in the database, + * to return all keys that were in the database from the time the scan started until the scan + * finishes (that is, {@link ClusterScanCursor#isFinished()} returns true). When the cursor is not + * needed, call {@link ClusterScanCursor#releaseCursorHandle()} to immediately free resources tied + * to the cursor. Note that this makes the cursor unusable in subsequent calls to scan. + * + *

This method guarantees that all keyslots available when the first SCAN is called will be + * scanned before the cursor is finished. Any keys added after the initial scan request is made + * are not guaranteed to be scanned. + * + *

The same key can be returned in multiple scans iteration. + * + * @see ClusterScanCursor for more details about how to use the cursor. * @see valkey.io for details. - * @param cursor The cursor object that wraps the scan state. To start a new scan, create a new - * empty ClusterScanCursor using {@link ClusterScanCursor#initalCursor()}. + * @param cursor The {@link ClusterScanCursor} object that wraps the scan state. To start a new + * scan, create a new empty ClusterScanCursor using {@link ClusterScanCursor#initalCursor()}. * @return An Array of Objects. The first element is always the {@link * ClusterScanCursor} for the next iteration of results. To see if there is more data on the * given cursor, call {@link ClusterScanCursor#isFinished()}. To release resources for the * current chunk immediately, call {@link ClusterScanCursor#releaseCursorHandle()} after using - * the cursor in a call to this method. The second element is an Array of Objects - * where each entry is a String representing a key. + * the cursor in a call to this method. The cursor cannot be used in a scan again after {@link + * ClusterScanCursor#releaseCursorHandle()} has been called. The second element is an + * Array of Objects where each entry is a String representing a key. * @example *

{@code
      * // Assume key contains a set with 200 keys
@@ -201,20 +213,32 @@ public interface GenericClusterCommands {
      * Using the same cursor object for multiple iterations will result in the same keys or unexpected
      * behavior. For more information about the Cluster Scan implementation, see Cluster
-     * Scan. As with the SCAN command, the method can be used to iterate over the keys in the
-     * database, to return all keys the database have from the time the scan started till the scan
-     * ends. The same key can be returned in multiple scans iteration.
+     * Scan.
+     *
+     * 

As with the SCAN command, the method can be used to iterate over the keys in the database, + * to return all keys that were in the database from the time the scan started until the scan + * finishes (that is, {@link ClusterScanCursor#isFinished()} returns true). When the cursor is not + * needed, call {@link ClusterScanCursor#releaseCursorHandle()} to immediately free resources tied + * to the cursor. Note that this makes the cursor unusable in subsequent calls to scan. + * + *

This method guarantees that all keyslots available when the first SCAN is called will be + * scanned before the cursor is finished. Any keys added after the initial scan request is made + * are not guaranteed to be scanned. + * + *

The same key can be returned in multiple scans iteration. * + * @see ClusterScanCursor for more details about how to use the cursor. * @see valkey.io for details. - * @param cursor The cursor object that wraps the scan state. To start a new scan, create a new - * empty ClusterScanCursor using {@link ClusterScanCursor#initalCursor()}. + * @param cursor The {@link ClusterScanCursor} object that wraps the scan state. To start a new + * scan, create a new empty ClusterScanCursor using {@link ClusterScanCursor#initalCursor()}. * @param options The {@link ScanOptions}. * @return An Array of Objects. The first element is always the {@link * ClusterScanCursor} for the next iteration of results. To see if there is more data on the * given cursor, call {@link ClusterScanCursor#isFinished()}. To release resources for the * current chunk immediately, call {@link ClusterScanCursor#releaseCursorHandle()} after using - * the cursor in a call to this method. The second element is an Array of Objects - * where each entry is a String representing a key. + * the cursor in a call to this method. The cursor cannot be used in a scan again after {@link + * ClusterScanCursor#releaseCursorHandle()} has been called. The second element is an + * Array of Objects where each entry is a String representing a key. * @example *

{@code
      * // Assume key contains a set with 200 keys
diff --git a/java/client/src/main/java/glide/api/logging/Logger.java b/java/client/src/main/java/glide/api/logging/Logger.java
index 744c5c2ddc..2b711b8d68 100644
--- a/java/client/src/main/java/glide/api/logging/Logger.java
+++ b/java/client/src/main/java/glide/api/logging/Logger.java
@@ -4,6 +4,9 @@
 import static glide.ffi.resolvers.LoggerResolver.initInternal;
 import static glide.ffi.resolvers.LoggerResolver.logInternal;
 
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.PrintStream;
 import java.util.function.Supplier;
 import lombok.Getter;
 import lombok.NonNull;
@@ -170,6 +173,31 @@ public static void log(
         logInternal(level.getLevel(), logIdentifier, message);
     }
 
+    /**
+     * Logs the provided exception or error if the provided log level is lower than the logger level.
+     *
+     * @param level The log level of the provided message.
+     * @param logIdentifier The log identifier should give the log a context.
+     * @param throwable The exception or error to log.
+     */
+    public static void log(
+            @NonNull Level level, @NonNull String logIdentifier, @NonNull Throwable throwable) {
+        // TODO: Add the corresponding API to Python and Node.js.
+        log(
+                level,
+                logIdentifier,
+                () -> {
+                    try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+                            PrintStream printStream = new PrintStream(outputStream)) {
+                        throwable.printStackTrace(printStream);
+                        return printStream.toString();
+                    } catch (IOException e) {
+                        // This can't happen with a ByteArrayOutputStream.
+                        return null;
+                    }
+                });
+    }
+
     /**
      * Creates a new logger instance and configure it with the provided log level and file name.
      *
diff --git a/java/client/src/main/java/glide/api/models/commands/scan/ClusterScanCursor.java b/java/client/src/main/java/glide/api/models/commands/scan/ClusterScanCursor.java
index 1ff5f88930..603e5c6661 100644
--- a/java/client/src/main/java/glide/api/models/commands/scan/ClusterScanCursor.java
+++ b/java/client/src/main/java/glide/api/models/commands/scan/ClusterScanCursor.java
@@ -16,9 +16,12 @@
  *     resources. These resources can be released by calling {@link #releaseCursorHandle()}. However
  *     doing so will disallow the cursor from being used in {@link GenericClusterCommands#scan} to
  *     get more data.
- *     

To do this safely, follow this procedure: 1. Call {@link GenericClusterCommands#scan} with - * the cursor. 2. Call {@link #releaseCursorHandle()} with the cursor. 3. Reassign the cursor to - * the cursor returned by {@link GenericClusterCommands#scan}. + *

To do this safely, follow this procedure: + *

    + *
  1. Call {@link GenericClusterCommands#scan} with the cursor. + *
  2. Call {@link #releaseCursorHandle()} with the cursor. + *
  3. Reassign the cursor to the cursor returned by {@link GenericClusterCommands#scan}. + *
*
{@code
  * ClusterScanCursor cursor = ClusterScanCursor.initialCursor();
  * Object[] result;
diff --git a/java/client/src/main/java/glide/api/models/commands/scan/ScanOptions.java b/java/client/src/main/java/glide/api/models/commands/scan/ScanOptions.java
index 7bfac1413c..c08c38dfac 100644
--- a/java/client/src/main/java/glide/api/models/commands/scan/ScanOptions.java
+++ b/java/client/src/main/java/glide/api/models/commands/scan/ScanOptions.java
@@ -34,8 +34,6 @@ public enum ObjectType {
         STREAM("Stream");
 
         /**
-         * Returns the name of the enum when communicating with the native layer.
-         *
          * @return the name of the enum when communicating with the native layer.
          */
         public String getNativeName() {
@@ -68,8 +66,6 @@ public boolean equals(Object o) {
     }
 
     /**
-     * Returns the pattern used for the MATCH filter.
-     *
      * @return the pattern used for the MATCH filter.
      */
     public String getMatchPattern() {
@@ -77,8 +73,6 @@ public String getMatchPattern() {
     }
 
     /**
-     * Returns the count used for the COUNT field. .
-     *
      * @return the count used for the COUNT field.
      */
     public Long getCount() {
@@ -86,8 +80,6 @@ public Long getCount() {
     }
 
     /**
-     * Returns the type used for the TYPE filter.
-     *
      * @return the type used for the TYPE filter.
      */
     public ObjectType getType() {
diff --git a/java/src/lib.rs b/java/src/lib.rs
index 4130540cde..46b253b898 100644
--- a/java/src/lib.rs
+++ b/java/src/lib.rs
@@ -427,7 +427,10 @@ pub extern "system" fn Java_glide_ffi_resolvers_ClusterScanCursorResolver_releas
 ) {
     handle_panics(
         move || {
-            fn release_native_cursor(env: &mut JNIEnv<'_>, cursor: JString) -> Result<(), FFIError> {
+            fn release_native_cursor(
+                env: &mut JNIEnv<'_>,
+                cursor: JString,
+            ) -> Result<(), FFIError> {
                 let cursor_str: String = env.get_string(&cursor)?.into();
                 glide_core::cluster_scan_container::remove_scan_state_cursor(cursor_str);
                 Ok(())