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
ofObjects
. 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 anArray
of Objects - * where each entry is aString
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 aString
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: + *
+ *
*- Call {@link GenericClusterCommands#scan} with the cursor. + *
- Call {@link #releaseCursorHandle()} with the cursor. + *
- 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 theMATCH
filter. - * * @return the pattern used for theMATCH
filter. */ public String getMatchPattern() { @@ -77,8 +73,6 @@ public String getMatchPattern() { } /** - * Returns the count used for theCOUNT
field. . - * * @return the count used for theCOUNT
field. */ public Long getCount() { @@ -86,8 +80,6 @@ public Long getCount() { } /** - * Returns the type used for theTYPE
filter. - * * @return the type used for theTYPE
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(())