diff --git a/.github/workflows/ort.yml b/.github/workflows/ort.yml index b6c0508eb8..d780c75d28 100644 --- a/.github/workflows/ort.yml +++ b/.github/workflows/ort.yml @@ -52,11 +52,6 @@ jobs: with: submodules: "true" ref: ${{ env.BASE_BRANCH }} - # This is a temporary fix, till ORT will fix thire issue with newer v of Cargo - https://github.com/oss-review-toolkit/ort/issues/8480 - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@1.76 - with: - targets: ${{ inputs.target }} - name: Set up JDK 11 for the ORT package uses: actions/setup-java@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d7cfa654b..b156030e04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ #### Fixes * Python: Fix typing error "‘type’ object is not subscriptable" ([#1203](https://github.com/aws/glide-for-redis/pull/1203)) +* Core: Fixed blocking commands to use the specified timeout from the command argument ([#1283](https://github.com/aws/glide-for-redis/pull/1283)) ## 0.3.3 (2024-03-28) diff --git a/csharp/tests/Integration/GetAndSet.cs b/csharp/tests/Integration/GetAndSet.cs index f5f7e72f7a..78c6a74180 100644 --- a/csharp/tests/Integration/GetAndSet.cs +++ b/csharp/tests/Integration/GetAndSet.cs @@ -92,7 +92,7 @@ public void ConcurrentOperationsWork() // TODO investigate and fix if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - Assert.Ignore("Flaky on MacOS"); + return; } using AsyncClient client = new("localhost", TestConfiguration.STANDALONE_PORTS[0], false); diff --git a/glide-core/src/client/mod.rs b/glide-core/src/client/mod.rs index 645bef1118..c679ad3397 100644 --- a/glide-core/src/client/mod.rs +++ b/glide-core/src/client/mod.rs @@ -8,9 +8,9 @@ use futures::FutureExt; use logger_core::log_info; use redis::aio::ConnectionLike; use redis::cluster_async::ClusterConnection; -use redis::cluster_routing::{RoutingInfo, SingleNodeRoutingInfo}; -use redis::RedisResult; +use redis::cluster_routing::{Routable, RoutingInfo, SingleNodeRoutingInfo}; use redis::{Cmd, ErrorKind, Value}; +use redis::{RedisError, RedisResult}; pub use standalone_client::StandaloneClient; use std::io; use std::ops::Deref; @@ -95,13 +95,122 @@ pub struct Client { } async fn run_with_timeout( - timeout: Duration, + timeout: Option, future: impl futures::Future> + Send, ) -> redis::RedisResult { - tokio::time::timeout(timeout, future) - .await - .map_err(|_| io::Error::from(io::ErrorKind::TimedOut).into()) - .and_then(|res| res) + match timeout { + Some(duration) => tokio::time::timeout(duration, future) + .await + .map_err(|_| io::Error::from(io::ErrorKind::TimedOut).into()) + .and_then(|res| res), + None => future.await, + } +} + +/// Extension to the request timeout for blocking commands to ensure we won't return with timeout error before the server responded +const BLOCKING_CMD_TIMEOUT_EXTENSION: f64 = 0.5; // seconds + +enum TimeUnit { + Milliseconds = 1000, + Seconds = 1, +} + +/// Enumeration representing different request timeout options. +#[derive(Default, PartialEq, Debug)] +enum RequestTimeoutOption { + // Indicates no timeout should be set for the request. + NoTimeout, + // Indicates the request timeout should be based on the client's configured timeout. + #[default] + ClientConfig, + // Indicates the request timeout should be based on the timeout specified in the blocking command. + BlockingCommand(Duration), +} + +/// Helper function for parsing a timeout argument to f64. +/// Attempts to parse the argument found at `timeout_idx` from bytes into an f64. +fn parse_timeout_to_f64(cmd: &Cmd, timeout_idx: usize) -> RedisResult { + let create_err = |err_msg| { + RedisError::from(( + ErrorKind::ResponseError, + err_msg, + format!( + "Expected to find timeout value at index {:?} for command {:?}. Recieved command = {:?}", + timeout_idx, + std::str::from_utf8(&cmd.command().unwrap_or_default()), + std::str::from_utf8(&cmd.get_packed_command()) + ), + )) + }; + let timeout_bytes = cmd + .arg_idx(timeout_idx) + .ok_or(create_err("Couldn't find timeout index"))?; + let timeout_str = std::str::from_utf8(timeout_bytes) + .map_err(|_| create_err("Failed to parse the timeout argument to string"))?; + timeout_str + .parse::() + .map_err(|_| create_err("Failed to parse the timeout argument to f64")) +} + +/// Attempts to get the timeout duration from the command argument at `timeout_idx`. +/// If the argument can be parsed into a duration, it returns the duration in seconds with BlockingCmdTimeout. +/// If the timeout argument value is zero, NoTimeout will be returned. Otherwise, ClientConfigTimeout is returned. +fn get_timeout_from_cmd_arg( + cmd: &Cmd, + timeout_idx: usize, + time_unit: TimeUnit, +) -> RedisResult { + let timeout_secs = parse_timeout_to_f64(cmd, timeout_idx)? / ((time_unit as i32) as f64); + if timeout_secs < 0.0 { + // Timeout cannot be negative, return the client's configured request timeout + Err(RedisError::from(( + ErrorKind::ResponseError, + "Timeout cannot be negative", + format!("Recieved timeout={:?}", timeout_secs), + ))) + } else if timeout_secs == 0.0 { + // `0` means we should set no timeout + Ok(RequestTimeoutOption::NoTimeout) + } else { + // We limit the maximum timeout due to restrictions imposed by Redis and the Duration crate + if timeout_secs > u32::MAX as f64 { + Err(RedisError::from(( + ErrorKind::ResponseError, + "Timeout is out of range, max timeout is 2^32 - 1 (u32::MAX)", + format!("Recieved timeout={:?}", timeout_secs), + ))) + } else { + // Extend the request timeout to ensure we don't timeout before receiving a response from the server. + Ok(RequestTimeoutOption::BlockingCommand( + Duration::from_secs_f64( + (timeout_secs + BLOCKING_CMD_TIMEOUT_EXTENSION).min(u32::MAX as f64), + ), + )) + } + } +} + +fn get_request_timeout(cmd: &Cmd, default_timeout: Duration) -> RedisResult> { + let command = cmd.command().unwrap_or_default(); + let timeout = match command.as_slice() { + b"BLPOP" | b"BRPOP" | b"BLMOVE" | b"BZPOPMAX" | b"BZPOPMIN" | b"BRPOPLPUSH" => { + get_timeout_from_cmd_arg(cmd, cmd.args_iter().len() - 1, TimeUnit::Seconds) + } + b"BLMPOP" | b"BZMPOP" => get_timeout_from_cmd_arg(cmd, 1, TimeUnit::Seconds), + b"XREAD" | b"XREADGROUP" => cmd + .position(b"BLOCK") + .map(|idx| get_timeout_from_cmd_arg(cmd, idx + 1, TimeUnit::Milliseconds)) + .unwrap_or(Ok(RequestTimeoutOption::ClientConfig)), + _ => Ok(RequestTimeoutOption::ClientConfig), + }?; + + match timeout { + RequestTimeoutOption::NoTimeout => Ok(None), + RequestTimeoutOption::ClientConfig => Ok(Some(default_timeout)), + RequestTimeoutOption::BlockingCommand(blocking_cmd_duration) => { + Ok(Some(blocking_cmd_duration)) + } + } } impl Client { @@ -111,7 +220,13 @@ impl Client { routing: Option, ) -> redis::RedisFuture<'a, Value> { let expected_type = expected_type_for_cmd(cmd); - run_with_timeout(self.request_timeout, async move { + let request_timeout = match get_request_timeout(cmd, self.request_timeout) { + Ok(request_timeout) => request_timeout, + Err(err) => { + return async { Err(err) }.boxed(); + } + }; + run_with_timeout(request_timeout, async move { match self.internal_client { ClientWrapper::Standalone(ref mut client) => client.send_command(cmd).await, @@ -189,7 +304,7 @@ impl Client { ) -> redis::RedisFuture<'a, Value> { let command_count = pipeline.cmd_iter().count(); let offset = command_count + 1; - run_with_timeout(self.request_timeout, async move { + run_with_timeout(Some(self.request_timeout), async move { let values = match self.internal_client { ClientWrapper::Standalone(ref mut client) => { client.send_pipeline(pipeline, offset, 1).await @@ -472,3 +587,153 @@ impl GlideClientForTests for StandaloneClient { self.send_command(cmd).boxed() } } + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use redis::Cmd; + + use crate::client::{ + get_request_timeout, RequestTimeoutOption, TimeUnit, BLOCKING_CMD_TIMEOUT_EXTENSION, + }; + + use super::get_timeout_from_cmd_arg; + + #[test] + fn test_get_timeout_from_cmd_returns_correct_duration_int() { + let mut cmd = Cmd::new(); + cmd.arg("BLPOP").arg("key1").arg("key2").arg("5"); + let result = get_timeout_from_cmd_arg(&cmd, cmd.args_iter().len() - 1, TimeUnit::Seconds); + assert!(result.is_ok()); + assert_eq!( + result.unwrap(), + RequestTimeoutOption::BlockingCommand(Duration::from_secs_f64( + 5.0 + BLOCKING_CMD_TIMEOUT_EXTENSION + )) + ); + } + + #[test] + fn test_get_timeout_from_cmd_returns_correct_duration_float() { + let mut cmd = Cmd::new(); + cmd.arg("BLPOP").arg("key1").arg("key2").arg(0.5); + let result = get_timeout_from_cmd_arg(&cmd, cmd.args_iter().len() - 1, TimeUnit::Seconds); + assert!(result.is_ok()); + assert_eq!( + result.unwrap(), + RequestTimeoutOption::BlockingCommand(Duration::from_secs_f64( + 0.5 + BLOCKING_CMD_TIMEOUT_EXTENSION + )) + ); + } + + #[test] + fn test_get_timeout_from_cmd_returns_correct_duration_milliseconds() { + let mut cmd = Cmd::new(); + cmd.arg("XREAD").arg("BLOCK").arg("500").arg("key"); + let result = get_timeout_from_cmd_arg(&cmd, 2, TimeUnit::Milliseconds); + assert!(result.is_ok()); + assert_eq!( + result.unwrap(), + RequestTimeoutOption::BlockingCommand(Duration::from_secs_f64( + 0.5 + BLOCKING_CMD_TIMEOUT_EXTENSION + )) + ); + } + + #[test] + fn test_get_timeout_from_cmd_returns_err_when_timeout_isnt_passed() { + let mut cmd = Cmd::new(); + cmd.arg("BLPOP").arg("key1").arg("key2").arg("key3"); + let result = get_timeout_from_cmd_arg(&cmd, cmd.args_iter().len() - 1, TimeUnit::Seconds); + assert!(result.is_err()); + let err = result.unwrap_err(); + println!("{:?}", err); + assert!(err.to_string().to_lowercase().contains("index"), "{err}"); + } + + #[test] + fn test_get_timeout_from_cmd_returns_err_when_timeout_is_larger_than_u32_max() { + let mut cmd = Cmd::new(); + cmd.arg("BLPOP") + .arg("key1") + .arg("key2") + .arg(u32::MAX as u64 + 1); + let result = get_timeout_from_cmd_arg(&cmd, cmd.args_iter().len() - 1, TimeUnit::Seconds); + assert!(result.is_err()); + let err = result.unwrap_err(); + println!("{:?}", err); + assert!(err.to_string().to_lowercase().contains("u32"), "{err}"); + } + + #[test] + fn test_get_timeout_from_cmd_returns_err_when_timeout_is_negative() { + let mut cmd = Cmd::new(); + cmd.arg("BLPOP").arg("key1").arg("key2").arg(-1); + let result = get_timeout_from_cmd_arg(&cmd, cmd.args_iter().len() - 1, TimeUnit::Seconds); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(err.to_string().to_lowercase().contains("negative"), "{err}"); + } + + #[test] + fn test_get_timeout_from_cmd_returns_no_timeout_when_zero_is_passed() { + let mut cmd = Cmd::new(); + cmd.arg("BLPOP").arg("key1").arg("key2").arg(0); + let result = get_timeout_from_cmd_arg(&cmd, cmd.args_iter().len() - 1, TimeUnit::Seconds); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), RequestTimeoutOption::NoTimeout,); + } + + #[test] + fn test_get_request_timeout_with_blocking_command_returns_cmd_arg_timeout() { + let mut cmd = Cmd::new(); + cmd.arg("BLPOP").arg("key1").arg("key2").arg("500"); + let result = get_request_timeout(&cmd, Duration::from_millis(100)); + assert!(result.is_ok()); + assert_eq!( + result.unwrap(), + Some(Duration::from_secs_f64( + 500.0 + BLOCKING_CMD_TIMEOUT_EXTENSION + )) + ); + + let mut cmd = Cmd::new(); + cmd.arg("XREADGROUP").arg("BLOCK").arg("500").arg("key"); + let result = get_request_timeout(&cmd, Duration::from_millis(100)); + assert!(result.is_ok()); + assert_eq!( + result.unwrap(), + Some(Duration::from_secs_f64( + 0.5 + BLOCKING_CMD_TIMEOUT_EXTENSION + )) + ); + + let mut cmd = Cmd::new(); + cmd.arg("BLMPOP").arg("0.857").arg("key"); + let result = get_request_timeout(&cmd, Duration::from_millis(100)); + assert!(result.is_ok()); + assert_eq!( + result.unwrap(), + Some(Duration::from_secs_f64( + 0.857 + BLOCKING_CMD_TIMEOUT_EXTENSION + )) + ); + } + + #[test] + fn test_get_request_timeout_non_blocking_command_returns_default_timeout() { + let mut cmd = Cmd::new(); + cmd.arg("SET").arg("key").arg("value").arg("PX").arg("500"); + let result = get_request_timeout(&cmd, Duration::from_millis(100)); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), Some(Duration::from_millis(100))); + + let mut cmd = Cmd::new(); + cmd.arg("XREADGROUP").arg("key"); + let result = get_request_timeout(&cmd, Duration::from_millis(100)); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), Some(Duration::from_millis(100))); + } +} diff --git a/glide-core/src/client/reconnecting_connection.rs b/glide-core/src/client/reconnecting_connection.rs index c039d347bd..d79a59c574 100644 --- a/glide-core/src/client/reconnecting_connection.rs +++ b/glide-core/src/client/reconnecting_connection.rs @@ -48,7 +48,7 @@ pub(super) struct ReconnectingConnection { async fn get_multiplexed_connection(client: &redis::Client) -> RedisResult { run_with_timeout( - DEFAULT_CONNECTION_ATTEMPT_TIMEOUT, + Some(DEFAULT_CONNECTION_ATTEMPT_TIMEOUT), client.get_multiplexed_async_connection(), ) .await diff --git a/glide-core/src/client/value_conversion.rs b/glide-core/src/client/value_conversion.rs index 2b47677dd7..e651a7d365 100644 --- a/glide-core/src/client/value_conversion.rs +++ b/glide-core/src/client/value_conversion.rs @@ -17,6 +17,7 @@ pub(crate) enum ExpectedReturnType { ZrankReturnType, JsonToggleReturnType, ArrayOfBools, + Lolwut, } pub(crate) fn convert_to_expected_type( @@ -178,6 +179,63 @@ pub(crate) fn convert_to_expected_type( ) .into()), }, + ExpectedReturnType::Lolwut => { + match value { + // cluster (multi-node) response - go recursive + Value::Map(map) => { + let result = map + .into_iter() + .map(|(key, inner_value)| { + let converted_key = convert_to_expected_type( + key, + Some(ExpectedReturnType::BulkString), + )?; + let converted_value = convert_to_expected_type( + inner_value, + Some(ExpectedReturnType::Lolwut), + )?; + Ok((converted_key, converted_value)) + }) + .collect::>(); + + result.map(Value::Map) + } + // RESP 2 response + Value::BulkString(bytes) => { + let text = std::str::from_utf8(&bytes).unwrap(); + let res = convert_lolwut_string(text); + Ok(Value::BulkString(Vec::from(res))) + } + // RESP 3 response + Value::VerbatimString { + format: _, + ref text, + } => { + let res = convert_lolwut_string(text); + Ok(Value::BulkString(Vec::from(res))) + } + _ => Err(( + ErrorKind::TypeError, + "LOLWUT response couldn't be converted to a user-friendly format", + (&format!("(response was {:?}...)", value)[..100]).into(), + ) + .into()), + } + } + } +} + +/// Convert string returned by `LOLWUT` command. +/// The input string is shell-friendly and contains color codes and escape sequences. +/// The output string is user-friendly, colored whitespaces replaced with corresponding symbols. +fn convert_lolwut_string(data: &str) -> String { + if data.contains("\x1b[0m") { + data.replace("\x1b[0;97;107m \x1b[0m", "\u{2591}") + .replace("\x1b[0;37;47m \x1b[0m", "\u{2592}") + .replace("\x1b[0;90;100m \x1b[0m", "\u{2593}") + .replace("\x1b[0;30;40m \x1b[0m", " ") + } else { + data.to_owned() } } @@ -261,6 +319,7 @@ pub(crate) fn expected_type_for_cmd(cmd: &Cmd) -> Option { None } } + b"LOLWUT" => Some(ExpectedReturnType::Lolwut), _ => None, } } @@ -269,6 +328,71 @@ pub(crate) fn expected_type_for_cmd(cmd: &Cmd) -> Option { mod tests { use super::*; + #[test] + fn convert_lolwut() { + assert!(matches!( + expected_type_for_cmd(redis::cmd("LOLWUT").arg("version").arg("42")), + Some(ExpectedReturnType::Lolwut) + )); + + let redis_string : String = "\x1b[0;97;107m \x1b[0m--\x1b[0;37;47m \x1b[0m--\x1b[0;90;100m \x1b[0m--\x1b[0;30;40m \x1b[0m".into(); + let expected: String = "\u{2591}--\u{2592}--\u{2593}-- ".into(); + + let converted_1 = convert_to_expected_type( + Value::BulkString(redis_string.clone().into_bytes()), + Some(ExpectedReturnType::Lolwut), + ); + assert_eq!( + Value::BulkString(expected.clone().into_bytes()), + converted_1.unwrap() + ); + + let converted_2 = convert_to_expected_type( + Value::VerbatimString { + format: redis::VerbatimFormat::Text, + text: redis_string.clone(), + }, + Some(ExpectedReturnType::Lolwut), + ); + assert_eq!( + Value::BulkString(expected.clone().into_bytes()), + converted_2.unwrap() + ); + + let converted_3 = convert_to_expected_type( + Value::Map(vec![ + ( + Value::SimpleString("node 1".into()), + Value::BulkString(redis_string.clone().into_bytes()), + ), + ( + Value::SimpleString("node 2".into()), + Value::BulkString(redis_string.clone().into_bytes()), + ), + ]), + Some(ExpectedReturnType::Lolwut), + ); + assert_eq!( + Value::Map(vec![ + ( + Value::BulkString("node 1".into()), + Value::BulkString(expected.clone().into_bytes()) + ), + ( + Value::BulkString("node 2".into()), + Value::BulkString(expected.clone().into_bytes()) + ), + ]), + converted_3.unwrap() + ); + + let converted_4 = convert_to_expected_type( + Value::SimpleString(redis_string.clone()), + Some(ExpectedReturnType::Lolwut), + ); + assert!(converted_4.is_err()); + } + #[test] fn convert_smismember() { assert!(matches!( diff --git a/glide-core/src/protobuf/redis_request.proto b/glide-core/src/protobuf/redis_request.proto index 8e65a24756..698dec1b4b 100644 --- a/glide-core/src/protobuf/redis_request.proto +++ b/glide-core/src/protobuf/redis_request.proto @@ -163,7 +163,10 @@ enum RequestType { GeoAdd = 121; GeoHash = 122; ObjectEncoding = 123; - GeoDist = 124; + SDiff = 124; + ObjectRefcount = 126; + LOLWUT = 100500; + GeoDist = 127; } message Command { diff --git a/glide-core/src/request_type.rs b/glide-core/src/request_type.rs index ab7a0d9025..b9b66f4e25 100644 --- a/glide-core/src/request_type.rs +++ b/glide-core/src/request_type.rs @@ -131,7 +131,10 @@ pub enum RequestType { GeoAdd = 121, GeoHash = 122, ObjectEncoding = 123, - GeoDist = 124, + SDiff = 124, + ObjectRefcount = 126, + LOLWUT = 100500, + GeoDist = 127, } fn get_two_word_command(first: &str, second: &str) -> Cmd { @@ -267,6 +270,9 @@ impl From<::protobuf::EnumOrUnknown> for RequestType { ProtobufRequestType::GeoHash => RequestType::GeoHash, ProtobufRequestType::ObjectEncoding => RequestType::ObjectEncoding, ProtobufRequestType::GeoDist => RequestType::GeoDist, + ProtobufRequestType::SDiff => RequestType::SDiff, + ProtobufRequestType::ObjectRefcount => RequestType::ObjectRefcount, + ProtobufRequestType::LOLWUT => RequestType::LOLWUT, } } } @@ -398,6 +404,9 @@ impl RequestType { RequestType::GeoHash => Some(cmd("GEOHASH")), RequestType::ObjectEncoding => Some(get_two_word_command("OBJECT", "ENCODING")), RequestType::GeoDist => Some(cmd("GEODIST")), + RequestType::SDiff => Some(cmd("SDIFF")), + RequestType::ObjectRefcount => Some(get_two_word_command("OBJECT", "REFCOUNT")), + RequestType::LOLWUT => Some(cmd("LOLWUT")), } } } diff --git a/glide-core/tests/test_client.rs b/glide-core/tests/test_client.rs index 945c9db504..475cf68888 100644 --- a/glide-core/tests/test_client.rs +++ b/glide-core/tests/test_client.rs @@ -6,7 +6,7 @@ mod utilities; #[cfg(test)] pub(crate) mod shared_client_tests { use super::*; - use glide_core::client::Client; + use glide_core::client::{Client, DEFAULT_RESPONSE_TIMEOUT}; use redis::{ cluster_routing::{MultipleNodeRoutingInfo, RoutingInfo}, FromRedisValue, InfoDict, RedisConnectionInfo, Value, @@ -21,6 +21,30 @@ pub(crate) mod shared_client_tests { client: Client, } + async fn create_client(server: &BackingServer, configuration: TestConfiguration) -> Client { + match server { + BackingServer::Standalone(server) => { + let connection_addr = server + .as_ref() + .map(|server| server.get_client_addr()) + .unwrap_or(get_shared_server_address(configuration.use_tls)); + + // TODO - this is a patch, handling the situation where the new server + // still isn't available to connection. This should be fixed in [RedisServer]. + repeat_try_create(|| async { + Client::new( + create_connection_request(&[connection_addr.clone()], &configuration) + .into(), + ) + .await + .ok() + }) + .await + } + BackingServer::Cluster(cluster) => create_cluster_client(cluster, configuration).await, + } + } + async fn setup_test_basics(use_cluster: bool, configuration: TestConfiguration) -> TestBasics { if use_cluster { let cluster_basics = cluster::setup_test_basics_internal(configuration).await; @@ -30,28 +54,9 @@ pub(crate) mod shared_client_tests { } } else { let test_basics = utilities::setup_test_basics_internal(&configuration).await; - - let connection_addr = test_basics - .server - .as_ref() - .map(|server| server.get_client_addr()) - .unwrap_or(get_shared_server_address(configuration.use_tls)); - - // TODO - this is a patch, handling the situation where the new server - // still isn't available to connection. This should be fixed in [RedisServer]. - let client = repeat_try_create(|| async { - Client::new( - create_connection_request(&[connection_addr.clone()], &configuration).into(), - ) - .await - .ok() - }) - .await; - - TestBasics { - server: BackingServer::Standalone(test_basics.server), - client, - } + let server = BackingServer::Standalone(test_basics.server); + let client = create_client(&server, configuration).await; + TestBasics { server, client } } } @@ -320,7 +325,44 @@ pub(crate) mod shared_client_tests { let mut test_basics = setup_test_basics( use_cluster, TestConfiguration { - request_timeout: Some(1), + request_timeout: Some(1), // milliseconds + shared_server: false, + ..Default::default() + }, + ) + .await; + let mut cmd = redis::Cmd::new(); + // Create a long running command to ensure we get into timeout + cmd.arg("EVAL") + .arg( + r#" + while (true) + do + redis.call('ping') + end + "#, + ) + .arg("0"); + let result = test_basics.client.send_command(&cmd, None).await; + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(err.is_timeout(), "{err}"); + }); + } + + #[rstest] + #[timeout(SHORT_CLUSTER_TEST_TIMEOUT)] + fn test_blocking_command_doesnt_raise_timeout_error(#[values(false, true)] use_cluster: bool) { + // We test that the request timeout is based on the value specified in the blocking command argument, + // and not on the one set in the client configuration. To achieve this, we execute a command designed to + // be blocked until it reaches the specified command timeout. We set the client's request timeout to + // a shorter duration than the blocking command's timeout. Subsequently, we confirm that we receive + // a response from the server instead of encountering a timeout error. + block_on_all(async { + let mut test_basics = setup_test_basics( + use_cluster, + TestConfiguration { + request_timeout: Some(1), // milliseconds shared_server: true, ..Default::default() }, @@ -328,11 +370,62 @@ pub(crate) mod shared_client_tests { .await; let mut cmd = redis::Cmd::new(); - cmd.arg("BLPOP").arg("foo").arg(0); // 0 timeout blocks indefinitely + cmd.arg("BLPOP").arg(generate_random_string(10)).arg(0.3); // server should return null after 300 millisecond + let result = test_basics.client.send_command(&cmd, None).await; + assert!(result.is_ok()); + assert_eq!(result.unwrap(), Value::Nil); + }); + } + + #[rstest] + #[timeout(SHORT_CLUSTER_TEST_TIMEOUT)] + fn test_blocking_command_with_negative_timeout_returns_error( + #[values(false, true)] use_cluster: bool, + ) { + // We test that when blocking command is passed with a negative timeout the command will return with an error + block_on_all(async { + let mut test_basics = setup_test_basics( + use_cluster, + TestConfiguration { + request_timeout: Some(1), // milliseconds + shared_server: true, + ..Default::default() + }, + ) + .await; + let mut cmd = redis::Cmd::new(); + cmd.arg("BLPOP").arg(generate_random_string(10)).arg(-1); let result = test_basics.client.send_command(&cmd, None).await; assert!(result.is_err()); let err = result.unwrap_err(); - assert!(err.is_timeout(), "{err}"); + assert_eq!(err.kind(), redis::ErrorKind::ResponseError); + assert!(err.to_string().contains("negative")); + }); + } + + #[rstest] + #[timeout(SHORT_CLUSTER_TEST_TIMEOUT)] + fn test_blocking_command_with_zero_timeout_blocks_indefinitely( + #[values(false, true)] use_cluster: bool, + ) { + // We test that when a blocking command is passed with a timeout duration of 0, it will block the client indefinitely + block_on_all(async { + let config = TestConfiguration { + request_timeout: Some(1), // millisecond + shared_server: true, + ..Default::default() + }; + let mut test_basics = setup_test_basics(use_cluster, config).await; + let key = generate_random_string(10); + let future = async move { + let mut cmd = redis::Cmd::new(); + cmd.arg("BLPOP").arg(key).arg(0); // `0` should block indefinitely + test_basics.client.send_command(&cmd, None).await + }; + // We execute the command with Tokio's timeout wrapper to prevent the test from hanging indefinitely. + let tokio_timeout_result = + tokio::time::timeout(DEFAULT_RESPONSE_TIMEOUT * 2, future).await; + assert!(tokio_timeout_result.is_err()); }); } diff --git a/glide-core/tests/utilities/cluster.rs b/glide-core/tests/utilities/cluster.rs index 6b524a9c51..9718915e28 100644 --- a/glide-core/tests/utilities/cluster.rs +++ b/glide-core/tests/utilities/cluster.rs @@ -229,18 +229,10 @@ async fn setup_acl_for_cluster( join_all(ops).await; } -pub async fn setup_test_basics_internal(mut configuration: TestConfiguration) -> ClusterTestBasics { - let cluster = if !configuration.shared_server { - Some(RedisCluster::new( - configuration.use_tls, - &configuration.connection_info, - None, - None, - )) - } else { - None - }; - +pub async fn create_cluster_client( + cluster: &Option, + mut configuration: TestConfiguration, +) -> Client { let addresses = if !configuration.shared_server { cluster.as_ref().unwrap().get_server_addresses() } else { @@ -257,7 +249,21 @@ pub async fn setup_test_basics_internal(mut configuration: TestConfiguration) -> configuration.request_timeout = configuration.request_timeout.or(Some(10000)); let connection_request = create_connection_request(&addresses, &configuration); - let client = Client::new(connection_request.into()).await.unwrap(); + Client::new(connection_request.into()).await.unwrap() +} + +pub async fn setup_test_basics_internal(configuration: TestConfiguration) -> ClusterTestBasics { + let cluster = if !configuration.shared_server { + Some(RedisCluster::new( + configuration.use_tls, + &configuration.connection_info, + None, + None, + )) + } else { + None + }; + let client = create_cluster_client(&cluster, configuration).await; ClusterTestBasics { cluster, client } } diff --git a/glide-core/tests/utilities/mod.rs b/glide-core/tests/utilities/mod.rs index f675b62c6b..04bd727a1d 100644 --- a/glide-core/tests/utilities/mod.rs +++ b/glide-core/tests/utilities/mod.rs @@ -609,7 +609,7 @@ pub async fn setup_acl(addr: &ConnectionAddr, connection_info: &RedisConnectionI connection.send_packed_command(&cmd).await.unwrap(); } -#[derive(Eq, PartialEq, Default)] +#[derive(Eq, PartialEq, Default, Clone)] pub enum ClusterMode { #[default] Disabled, @@ -652,7 +652,7 @@ pub fn create_connection_request( connection_request } -#[derive(Default)] +#[derive(Default, Clone)] pub struct TestConfiguration { pub use_tls: bool, pub connection_retry_strategy: Option, diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index f9812dd4fb..bed04e88e4 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -42,6 +42,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.MGet; import static redis_request.RedisRequestOuterClass.RequestType.MSet; import static redis_request.RedisRequestOuterClass.RequestType.ObjectEncoding; +import static redis_request.RedisRequestOuterClass.RequestType.ObjectRefcount; import static redis_request.RedisRequestOuterClass.RequestType.PExpire; import static redis_request.RedisRequestOuterClass.RequestType.PExpireAt; import static redis_request.RedisRequestOuterClass.RequestType.PTTL; @@ -54,6 +55,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.RPushX; import static redis_request.RedisRequestOuterClass.RequestType.SAdd; import static redis_request.RedisRequestOuterClass.RequestType.SCard; +import static redis_request.RedisRequestOuterClass.RequestType.SDiff; import static redis_request.RedisRequestOuterClass.RequestType.SDiffStore; import static redis_request.RedisRequestOuterClass.RequestType.SInter; import static redis_request.RedisRequestOuterClass.RequestType.SInterStore; @@ -341,6 +343,12 @@ public CompletableFuture objectEncoding(@NonNull String key) { ObjectEncoding, new String[] {key}, this::handleStringOrNullResponse); } + @Override + public CompletableFuture objectRefcount(@NonNull String key) { + return commandManager.submitNewCommand( + ObjectRefcount, new String[] {key}, this::handleLongOrNullResponse); + } + @Override public CompletableFuture incr(@NonNull String key) { return commandManager.submitNewCommand(Incr, new String[] {key}, this::handleLongResponse); @@ -559,6 +567,11 @@ public CompletableFuture scard(@NonNull String key) { return commandManager.submitNewCommand(SCard, new String[] {key}, this::handleLongResponse); } + @Override + public CompletableFuture> sdiff(@NonNull String[] keys) { + return commandManager.submitNewCommand(SDiff, keys, this::handleSetResponse); + } + @Override public CompletableFuture smismember(@NonNull String key, @NonNull String[] members) { String[] arguments = ArrayUtils.addFirst(members, key); diff --git a/java/client/src/main/java/glide/api/RedisClient.java b/java/client/src/main/java/glide/api/RedisClient.java index 2d1aeda088..b58c392811 100644 --- a/java/client/src/main/java/glide/api/RedisClient.java +++ b/java/client/src/main/java/glide/api/RedisClient.java @@ -2,6 +2,7 @@ package glide.api; import static glide.utils.ArrayTransformUtils.castArray; +import static glide.utils.ArrayTransformUtils.concatenateArrays; import static glide.utils.ArrayTransformUtils.convertMapToKeyValueStringArray; import static redis_request.RedisRequestOuterClass.RequestType.ClientGetName; import static redis_request.RedisRequestOuterClass.RequestType.ClientId; @@ -12,6 +13,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.CustomCommand; import static redis_request.RedisRequestOuterClass.RequestType.Echo; import static redis_request.RedisRequestOuterClass.RequestType.Info; +import static redis_request.RedisRequestOuterClass.RequestType.LOLWUT; import static redis_request.RedisRequestOuterClass.RequestType.LastSave; import static redis_request.RedisRequestOuterClass.RequestType.Ping; import static redis_request.RedisRequestOuterClass.RequestType.Select; @@ -25,6 +27,7 @@ import glide.api.models.configuration.RedisClientConfiguration; import glide.managers.CommandManager; import glide.managers.ConnectionManager; +import java.util.Arrays; import java.util.Map; import java.util.concurrent.CompletableFuture; import lombok.NonNull; @@ -138,4 +141,33 @@ public CompletableFuture time() { public CompletableFuture lastsave() { return commandManager.submitNewCommand(LastSave, new String[0], this::handleLongResponse); } + + @Override + public CompletableFuture lolwut() { + return commandManager.submitNewCommand(LOLWUT, new String[0], this::handleStringResponse); + } + + @Override + public CompletableFuture lolwut(int @NonNull [] parameters) { + String[] arguments = + Arrays.stream(parameters).mapToObj(Integer::toString).toArray(String[]::new); + return commandManager.submitNewCommand(LOLWUT, arguments, this::handleStringResponse); + } + + @Override + public CompletableFuture lolwut(int version) { + return commandManager.submitNewCommand( + LOLWUT, + new String[] {VERSION_REDIS_API, Integer.toString(version)}, + this::handleStringResponse); + } + + @Override + public CompletableFuture lolwut(int version, int @NonNull [] parameters) { + String[] arguments = + concatenateArrays( + new String[] {VERSION_REDIS_API, Integer.toString(version)}, + Arrays.stream(parameters).mapToObj(Integer::toString).toArray(String[]::new)); + return commandManager.submitNewCommand(LOLWUT, arguments, this::handleStringResponse); + } } diff --git a/java/client/src/main/java/glide/api/RedisClusterClient.java b/java/client/src/main/java/glide/api/RedisClusterClient.java index c2aec33d34..65e00e0059 100644 --- a/java/client/src/main/java/glide/api/RedisClusterClient.java +++ b/java/client/src/main/java/glide/api/RedisClusterClient.java @@ -1,8 +1,10 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api; +import static glide.api.commands.ServerManagementCommands.VERSION_REDIS_API; import static glide.utils.ArrayTransformUtils.castArray; import static glide.utils.ArrayTransformUtils.castMapOfArrays; +import static glide.utils.ArrayTransformUtils.concatenateArrays; import static glide.utils.ArrayTransformUtils.convertMapToKeyValueStringArray; import static redis_request.RedisRequestOuterClass.RequestType.ClientGetName; import static redis_request.RedisRequestOuterClass.RequestType.ClientId; @@ -13,6 +15,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.CustomCommand; import static redis_request.RedisRequestOuterClass.RequestType.Echo; import static redis_request.RedisRequestOuterClass.RequestType.Info; +import static redis_request.RedisRequestOuterClass.RequestType.LOLWUT; import static redis_request.RedisRequestOuterClass.RequestType.LastSave; import static redis_request.RedisRequestOuterClass.RequestType.Ping; import static redis_request.RedisRequestOuterClass.RequestType.Time; @@ -28,6 +31,7 @@ import glide.api.models.configuration.RequestRoutingConfiguration.SingleNodeRoute; import glide.managers.CommandManager; import glide.managers.ConnectionManager; +import java.util.Arrays; import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; @@ -297,4 +301,89 @@ public CompletableFuture> lastsave(@NonNull Route route) { ? ClusterValue.of(handleLongResponse(response)) : ClusterValue.of(handleMapResponse(response))); } + + @Override + public CompletableFuture lolwut() { + return commandManager.submitNewCommand(LOLWUT, new String[0], this::handleStringResponse); + } + + @Override + public CompletableFuture lolwut(int @NonNull [] parameters) { + String[] arguments = + Arrays.stream(parameters).mapToObj(Integer::toString).toArray(String[]::new); + return commandManager.submitNewCommand(LOLWUT, arguments, this::handleStringResponse); + } + + @Override + public CompletableFuture lolwut(int version) { + return commandManager.submitNewCommand( + LOLWUT, + new String[] {VERSION_REDIS_API, Integer.toString(version)}, + this::handleStringResponse); + } + + @Override + public CompletableFuture lolwut(int version, int @NonNull [] parameters) { + String[] arguments = + concatenateArrays( + new String[] {VERSION_REDIS_API, Integer.toString(version)}, + Arrays.stream(parameters).mapToObj(Integer::toString).toArray(String[]::new)); + return commandManager.submitNewCommand(LOLWUT, arguments, this::handleStringResponse); + } + + @Override + public CompletableFuture> lolwut(@NonNull Route route) { + return commandManager.submitNewCommand( + LOLWUT, + new String[0], + route, + response -> + route instanceof SingleNodeRoute + ? ClusterValue.ofSingleValue(handleStringResponse(response)) + : ClusterValue.ofMultiValue(handleMapResponse(response))); + } + + @Override + public CompletableFuture> lolwut( + int @NonNull [] parameters, @NonNull Route route) { + String[] arguments = + Arrays.stream(parameters).mapToObj(Integer::toString).toArray(String[]::new); + return commandManager.submitNewCommand( + LOLWUT, + arguments, + route, + response -> + route instanceof SingleNodeRoute + ? ClusterValue.ofSingleValue(handleStringResponse(response)) + : ClusterValue.ofMultiValue(handleMapResponse(response))); + } + + @Override + public CompletableFuture> lolwut(int version, @NonNull Route route) { + return commandManager.submitNewCommand( + LOLWUT, + new String[] {VERSION_REDIS_API, Integer.toString(version)}, + route, + response -> + route instanceof SingleNodeRoute + ? ClusterValue.ofSingleValue(handleStringResponse(response)) + : ClusterValue.ofMultiValue(handleMapResponse(response))); + } + + @Override + public CompletableFuture> lolwut( + int version, int @NonNull [] parameters, @NonNull Route route) { + String[] arguments = + concatenateArrays( + new String[] {VERSION_REDIS_API, Integer.toString(version)}, + Arrays.stream(parameters).mapToObj(Integer::toString).toArray(String[]::new)); + return commandManager.submitNewCommand( + LOLWUT, + arguments, + route, + response -> + route instanceof SingleNodeRoute + ? ClusterValue.ofSingleValue(handleStringResponse(response)) + : ClusterValue.ofMultiValue(handleMapResponse(response))); + } } diff --git a/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java b/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java index ede7fede20..ea8030679b 100644 --- a/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java @@ -389,4 +389,22 @@ CompletableFuture pexpireAt( * } */ CompletableFuture objectEncoding(String key); + + /** + * Returns the reference count of the object stored at key. + * + * @see redis.io for details. + * @param key The key of the object to get the reference count of. + * @return If key exists, returns the reference count of the object stored at + * key as a Long. Otherwise, returns null. + * @example + *
{@code
+     * Long refcount = client.objectRefcount("my_hash").get();
+     * assert refcount == 2L;
+     *
+     * refcount = client.objectRefcount("non_existing_key").get();
+     * assert refcount == null;
+     * }
+ */ + CompletableFuture objectRefcount(String key); } diff --git a/java/client/src/main/java/glide/api/commands/ListBaseCommands.java b/java/client/src/main/java/glide/api/commands/ListBaseCommands.java index c85b85fb68..0be2be52f1 100644 --- a/java/client/src/main/java/glide/api/commands/ListBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/ListBaseCommands.java @@ -286,11 +286,12 @@ CompletableFuture linsert( * href="https://github.com/aws/glide-for-redis/wiki/General-Concepts#blocking-commands">Blocking * Commands for more details and best practices. * @param keys The keys of the lists to pop from. - * @param timeout The number of seconds to wait for a blocking BLPOP operation to - * complete. A value of 0 will block indefinitely. - * @return An array containing the key from which the element was popped - * and the value of the popped element, formatted as [key, value]. - * If no element could be popped and the timeout expired, returns null. + * @param timeout The number of seconds to wait for a blocking operation to complete. A value of + * 0 will block indefinitely. + * @return A two-element array containing the key from which the element + * was popped and the value of the popped element, formatted as + * [key, value]. If no element could be popped and the timeout expired, returns + * null. * @example *
{@code
      * String[] response = client.blpop(["list1", "list2"], 0.5).get();
@@ -310,11 +311,12 @@ CompletableFuture linsert(
      *     href="https://github.com/aws/glide-for-redis/wiki/General-Concepts#blocking-commands">Blocking
      *     Commands for more details and best practices.
      * @param keys The keys of the lists to pop from.
-     * @param timeout The number of seconds to wait for a blocking BRPOP operation to
-     *     complete. A value of 0 will block indefinitely.
-     * @return An array containing the key from which the element was popped
-     *     and the value of the popped element, formatted as [key, value].
-     *     If no element could be popped and the timeout expired, returns null.
+     * @param timeout The number of seconds to wait for a blocking operation to complete. A value of
+     *     0 will block indefinitely.
+     * @return A two-element array containing the key from which the element
+     *     was popped and the value of the popped element, formatted as 
+     *     [key, value]. If no element could be popped and the timeout expired, returns 
+     *     null.
      * @example
      *     
{@code
      * String[] response = client.brpop(["list1", "list2"], 0.5).get();
diff --git a/java/client/src/main/java/glide/api/commands/ServerManagementClusterCommands.java b/java/client/src/main/java/glide/api/commands/ServerManagementClusterCommands.java
index db05056651..ba7d8a0c20 100644
--- a/java/client/src/main/java/glide/api/commands/ServerManagementClusterCommands.java
+++ b/java/client/src/main/java/glide/api/commands/ServerManagementClusterCommands.java
@@ -315,4 +315,183 @@ public interface ServerManagementClusterCommands {
      * }
*/ CompletableFuture> lastsave(Route route); + + /** + * Displays a piece of generative computer art and the Redis version.
+ * The command will be routed to a random node. + * + * @see redis.io for details. + * @return A piece of generative computer art along with the current Redis version. + * @example + *
{@code
+     * String data = client.lolwut().get();
+     * System.out.println(data);
+     * assert data.contains("Redis ver. 7.2.3");
+     * }
+ */ + CompletableFuture lolwut(); + + /** + * Displays a piece of generative computer art and the Redis version.
+ * The command will be routed to a random node. + * + * @see redis.io for details. + * @param parameters Additional set of arguments in order to change the output: + *
    + *
  • On Redis version 5, those are length of the line, number of squares per + * row, and number of squares per column. + *
  • On Redis version 6, those are number of columns and number of lines. + *
  • On other versions parameters are ignored. + *
+ * + * @return A piece of generative computer art along with the current Redis version. + * @example + *
{@code
+     * String data = client.lolwut(new int[] { 40, 20 }).get();
+     * System.out.println(data);
+     * assert data.contains("Redis ver. 7.2.3");
+     * }
+ */ + CompletableFuture lolwut(int[] parameters); + + /** + * Displays a piece of generative computer art and the Redis version.
+ * The command will be routed to a random node. + * + * @apiNote Versions 5 and 6 produce graphical things. + * @see redis.io for details. + * @param version Version of computer art to generate. + * @return A piece of generative computer art along with the current Redis version. + * @example + *
{@code
+     * String data = client.lolwut(6).get();
+     * System.out.println(data);
+     * assert data.contains("Redis ver. 7.2.3");
+     * }
+ */ + CompletableFuture lolwut(int version); + + /** + * Displays a piece of generative computer art and the Redis version.
+ * The command will be routed to a random node. + * + * @apiNote Versions 5 and 6 produce graphical things. + * @see redis.io for details. + * @param version Version of computer art to generate. + * @param parameters Additional set of arguments in order to change the output: + *
    + *
  • For version 5, those are length of the line, number of squares per row, + * and number of squares per column. + *
  • For version 6, those are number of columns and number of lines. + *
+ * + * @return A piece of generative computer art along with the current Redis version. + * @example + *
{@code
+     * String data = client.lolwut(6, new int[] { 40, 20 }).get();
+     * System.out.println(data);
+     * assert data.contains("Redis ver. 7.2.3");
+     * data = client.lolwut(5, new int[] { 30, 5, 5 }).get();
+     * System.out.println(data);
+     * assert data.contains("Redis ver. 7.2.3");
+     *
+     * }
+ */ + CompletableFuture lolwut(int version, int[] parameters); + + /** + * Displays a piece of generative computer art and the Redis version. + * + * @see redis.io for details. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return A piece of generative computer art along with the current Redis version. + * @example + *
{@code
+     * ClusterValue response = client.lolwut(ALL_NODES).get();
+     * for (String data : response.getMultiValue().values()) {
+     *     System.out.println(data);
+     *     assert data.contains("Redis ver. 7.2.3");
+     * }
+     * }
+ */ + CompletableFuture> lolwut(Route route); + + /** + * Displays a piece of generative computer art and the Redis version. + * + * @see redis.io for details. + * @param parameters Additional set of arguments in order to change the output: + *
    + *
  • On Redis version 5, those are length of the line, number of squares per + * row, and number of squares per column. + *
  • On Redis version 6, those are number of columns and number of lines. + *
  • On other versions parameters are ignored. + *
+ * + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return A piece of generative computer art along with the current Redis version. + * @example + *
{@code
+     * String data = client.lolwut(new int[] { 40, 20 }, ALL_NODES).get();
+     * for (String data : response.getMultiValue().values()) {
+     *     System.out.println(data);
+     *     assert data.contains("Redis ver. 7.2.3");
+     * }
+     * }
+ */ + CompletableFuture> lolwut(int[] parameters, Route route); + + /** + * Displays a piece of generative computer art and the Redis version. + * + * @apiNote Versions 5 and 6 produce graphical things. + * @see redis.io for details. + * @param version Version of computer art to generate. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return A piece of generative computer art along with the current Redis version. + * @example + *
{@code
+     * ClusterValue response = client.lolwut(6, ALL_NODES).get();
+     * for (String data : response.getMultiValue().values()) {
+     *     System.out.println(data);
+     *     assert data.contains("Redis ver. 7.2.3");
+     * }
+     * }
+ */ + CompletableFuture> lolwut(int version, Route route); + + /** + * Displays a piece of generative computer art and the Redis version. + * + * @apiNote Versions 5 and 6 produce graphical things. + * @see redis.io for details. + * @param version Version of computer art to generate. + * @param parameters Additional set of arguments in order to change the output: + *
    + *
  • For version 5, those are length of the line, number of squares per row, + * and number of squares per column. + *
  • For version 6, those are number of columns and number of lines. + *
+ * + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return A piece of generative computer art along with the current Redis version. + * @example + *
{@code
+     * String data = client.lolwut(6, new int[] { 40, 20 }, ALL_NODES).get();
+     * for (String data : response.getMultiValue().values()) {
+     *     System.out.println(data);
+     *     assert data.contains("Redis ver. 7.2.3");
+     * }
+     * data = client.lolwut(5, new int[] { 30, 5, 5 }, ALL_NODES).get();
+     * for (String data : response.getMultiValue().values()) {
+     *     System.out.println(data);
+     *     assert data.contains("Redis ver. 7.2.3");
+     * }
+     * }
+ */ + CompletableFuture> lolwut(int version, int[] parameters, Route route); } diff --git a/java/client/src/main/java/glide/api/commands/ServerManagementCommands.java b/java/client/src/main/java/glide/api/commands/ServerManagementCommands.java index 30dd8f15a2..fce95f37b6 100644 --- a/java/client/src/main/java/glide/api/commands/ServerManagementCommands.java +++ b/java/client/src/main/java/glide/api/commands/ServerManagementCommands.java @@ -13,6 +13,9 @@ */ public interface ServerManagementCommands { + /** A keyword for {@link #lolwut(int)} and {@link #lolwut(int, int[])}. */ + String VERSION_REDIS_API = "VERSION"; + /** * Gets information and statistics about the Redis server using the {@link Section#DEFAULT} * option. @@ -147,4 +150,82 @@ public interface ServerManagementCommands { * }
*/ CompletableFuture lastsave(); + + /** + * Displays a piece of generative computer art and the Redis version. + * + * @see redis.io for details. + * @return A piece of generative computer art along with the current Redis version. + * @example + *
{@code
+     * String data = client.lolwut().get();
+     * System.out.println(data);
+     * assert data.contains("Redis ver. 7.2.3");
+     * }
+ */ + CompletableFuture lolwut(); + + /** + * Displays a piece of generative computer art and the Redis version. + * + * @see redis.io for details. + * @param parameters Additional set of arguments in order to change the output: + *
    + *
  • On Redis version 5, those are length of the line, number of squares per + * row, and number of squares per column. + *
  • On Redis version 6, those are number of columns and number of lines. + *
  • On other versions parameters are ignored. + *
+ * + * @return A piece of generative computer art along with the current Redis version. + * @example + *
{@code
+     * String data = client.lolwut(new int[] { 40, 20 }).get();
+     * System.out.println(data);
+     * assert data.contains("Redis ver. 7.2.3");
+     * }
+ */ + CompletableFuture lolwut(int[] parameters); + + /** + * Displays a piece of generative computer art and the Redis version. + * + * @apiNote Versions 5 and 6 produce graphical things. + * @see redis.io for details. + * @param version Version of computer art to generate. + * @return A piece of generative computer art along with the current Redis version. + * @example + *
{@code
+     * String data = client.lolwut(6).get();
+     * System.out.println(data);
+     * assert data.contains("Redis ver. 7.2.3");
+     * }
+ */ + CompletableFuture lolwut(int version); + + /** + * Displays a piece of generative computer art and the Redis version. + * + * @apiNote Versions 5 and 6 produce graphical things. + * @see redis.io for details. + * @param version Version of computer art to generate. + * @param parameters Additional set of arguments in order to change the output: + *
    + *
  • For version 5, those are length of the line, number of squares per row, + * and number of squares per column. + *
  • For version 6, those are number of columns and number of lines. + *
+ * + * @return A piece of generative computer art along with the current Redis version. + * @example + *
{@code
+     * String data = client.lolwut(6, new int[] { 40, 20 }).get();
+     * System.out.println(data);
+     * assert data.contains("Redis ver. 7.2.3");
+     * data = client.lolwut(5, new int[] { 30, 5, 5 }).get();
+     * System.out.println(data);
+     * assert data.contains("Redis ver. 7.2.3");
+     * }
+ */ + CompletableFuture lolwut(int version, int[] parameters); } diff --git a/java/client/src/main/java/glide/api/commands/SetBaseCommands.java b/java/client/src/main/java/glide/api/commands/SetBaseCommands.java index a143d2e82f..74ce5edf0e 100644 --- a/java/client/src/main/java/glide/api/commands/SetBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/SetBaseCommands.java @@ -133,6 +133,23 @@ public interface SetBaseCommands { */ CompletableFuture sismember(String key, String member); + /** + * Computes the difference between the first set and all the successive sets in keys. + * + * @apiNote When in cluster mode, all keys must map to the same hash slot + * . + * @see redis.io for details. + * @param keys The keys of the sets to diff. + * @return A Set of elements representing the difference between the sets.
+ * If the a key does not exist, it is treated as an empty set. + * @example + *
{@code
+     * Set values = client.sdiff(new String[] {"set1", "set2"}).get();
+     * assert values.contains("element"); // Indicates that "element" is present in "set1", but missing in "set2"
+     * }
+ */ + CompletableFuture> sdiff(String[] keys); + /** * Stores the difference between the first set and all the successive sets in keys * into a new set at destination. diff --git a/java/client/src/main/java/glide/api/models/BaseTransaction.java b/java/client/src/main/java/glide/api/models/BaseTransaction.java index 16be2fd746..94ac634cc4 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -1,6 +1,7 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models; +import static glide.api.commands.ServerManagementCommands.VERSION_REDIS_API; import static glide.api.commands.SortedSetBaseCommands.WITH_SCORES_REDIS_API; import static glide.api.commands.SortedSetBaseCommands.WITH_SCORE_REDIS_API; import static glide.api.models.commands.RangeOptions.createZRangeArgs; @@ -42,6 +43,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.Info; import static redis_request.RedisRequestOuterClass.RequestType.LInsert; import static redis_request.RedisRequestOuterClass.RequestType.LLen; +import static redis_request.RedisRequestOuterClass.RequestType.LOLWUT; import static redis_request.RedisRequestOuterClass.RequestType.LPop; import static redis_request.RedisRequestOuterClass.RequestType.LPush; import static redis_request.RedisRequestOuterClass.RequestType.LPushX; @@ -53,6 +55,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.MGet; import static redis_request.RedisRequestOuterClass.RequestType.MSet; import static redis_request.RedisRequestOuterClass.RequestType.ObjectEncoding; +import static redis_request.RedisRequestOuterClass.RequestType.ObjectRefcount; import static redis_request.RedisRequestOuterClass.RequestType.PExpire; import static redis_request.RedisRequestOuterClass.RequestType.PExpireAt; import static redis_request.RedisRequestOuterClass.RequestType.PTTL; @@ -66,6 +69,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.RPushX; import static redis_request.RedisRequestOuterClass.RequestType.SAdd; import static redis_request.RedisRequestOuterClass.RequestType.SCard; +import static redis_request.RedisRequestOuterClass.RequestType.SDiff; import static redis_request.RedisRequestOuterClass.RequestType.SDiffStore; import static redis_request.RedisRequestOuterClass.RequestType.SInter; import static redis_request.RedisRequestOuterClass.RequestType.SInterStore; @@ -123,6 +127,7 @@ import glide.api.models.commands.StreamAddOptions; import glide.api.models.commands.StreamAddOptions.StreamAddOptionsBuilder; import glide.api.models.commands.ZaddOptions; +import java.util.Arrays; import java.util.Map; import lombok.Getter; import lombok.NonNull; @@ -933,6 +938,21 @@ public T scard(@NonNull String key) { return getThis(); } + /** + * Computes the difference between the first set and all the successive sets in keys. + * + * @see redis.io for details. + * @param keys The keys of the sets to diff. + * @return Command Response - A Set of elements representing the difference between + * the sets.
+ * If the a key does not exist, it is treated as an empty set. + */ + public T sdiff(@NonNull String[] keys) { + ArgsArray commandArgs = buildArgs(keys); + protobufTransaction.addCommands(buildCommand(SDiff, commandArgs)); + return getThis(); + } + /** * Checks whether each member is contained in the members of the set stored at key. * @@ -1961,6 +1981,80 @@ public T lastsave() { return getThis(); } + /** + * Displays a piece of generative computer art and the Redis version. + * + * @see redis.io for details. + * @return Command Response - A piece of generative computer art along with the current Redis + * version. + */ + public T lolwut() { + protobufTransaction.addCommands(buildCommand(LOLWUT)); + return getThis(); + } + + /** + * Displays a piece of generative computer art and the Redis version. + * + * @see redis.io for details. + * @param parameters Additional set of arguments in order to change the output: + *
    + *
  • On Redis version 5, those are length of the line, number of squares per + * row, and number of squares per column. + *
  • On Redis version 6, those are number of columns and number of lines. + *
  • On other versions parameters are ignored. + *
+ * + * @return Command Response - A piece of generative computer art along with the current Redis + * version. + */ + public T lolwut(int @NonNull [] parameters) { + String[] arguments = + Arrays.stream(parameters).mapToObj(Integer::toString).toArray(String[]::new); + protobufTransaction.addCommands(buildCommand(LOLWUT, buildArgs(arguments))); + return getThis(); + } + + /** + * Displays a piece of generative computer art and the Redis version. + * + * @apiNote Versions 5 and 6 produce graphical things. + * @see redis.io for details. + * @param version Version of computer art to generate. + * @return Command Response - A piece of generative computer art along with the current Redis + * version. + */ + public T lolwut(int version) { + ArgsArray commandArgs = buildArgs(VERSION_REDIS_API, Integer.toString(version)); + protobufTransaction.addCommands(buildCommand(LOLWUT, commandArgs)); + return getThis(); + } + + /** + * Displays a piece of generative computer art and the Redis version. + * + * @apiNote Versions 5 and 6 produce graphical things. + * @see redis.io for details. + * @param version Version of computer art to generate. + * @param parameters Additional set of arguments in order to change the output: + *
    + *
  • For version 5, those are length of the line, number of squares per row, + * and number of squares per column. + *
  • For version 6, those are number of columns and number of lines. + *
+ * + * @return Command Response - A piece of generative computer art along with the current Redis + * version. + */ + public T lolwut(int version, int @NonNull [] parameters) { + String[] arguments = + concatenateArrays( + new String[] {VERSION_REDIS_API, Integer.toString(version)}, + Arrays.stream(parameters).mapToObj(Integer::toString).toArray(String[]::new)); + protobufTransaction.addCommands(buildCommand(LOLWUT, buildArgs(arguments))); + return getThis(); + } + /** * Returns the string representation of the type of the value stored at key. * @@ -2009,10 +2103,11 @@ public T linsert( * href="https://github.com/aws/glide-for-redis/wiki/General-Concepts#blocking-commands">Blocking * Commands for more details and best practices. * @param keys The keys of the lists to pop from. - * @param timeout The number of seconds to wait for a blocking BRPOP operation to - * complete. A value of 0 will block indefinitely. - * @return Command Response - An array containing the key from which the - * element was popped and the value of the popped element, formatted as + * @param timeout The number of seconds to wait for a blocking operation to complete. A value of + * 0 will block indefinitely. + * @return Command Response - A two-element array containing the key + * from which the element was popped and the value of the popped element, + * formatted as * [key, value]. If no element could be popped and the timeout expired, returns * null. */ @@ -2062,10 +2157,11 @@ public T rpushx(@NonNull String key, @NonNull String[] elements) { * href="https://github.com/aws/glide-for-redis/wiki/General-Concepts#blocking-commands">Blocking * Commands for more details and best practices. * @param keys The keys of the lists to pop from. - * @param timeout The number of seconds to wait for a blocking BLPOP operation to - * complete. A value of 0 will block indefinitely. - * @return Command Response - An array containing the key from which the - * element was popped and the value of the popped element, formatted as + * @param timeout The number of seconds to wait for a blocking operation to complete. A value of + * 0 will block indefinitely. + * @return Command Response - A two-element array containing the key + * from which the element was popped and the value of the popped element, + * formatted as * [key, value]. If no element could be popped and the timeout expired, returns * null. */ @@ -2240,6 +2336,21 @@ public T objectEncoding(@NonNull String key) { return getThis(); } + /** + * Returns the reference count of the object stored at key. + * + * @see redis.io for details. + * @param key The key of the object to get the reference count of. + * @return Command response - If key exists, returns the reference count of the + * object stored at key as a Long. Otherwise, returns null + * . + */ + public T objectRefcount(@NonNull String key) { + ArgsArray commandArgs = buildArgs(key); + protobufTransaction.addCommands(buildCommand(ObjectRefcount, commandArgs)); + return getThis(); + } + /** Build protobuf {@link Command} object for given command and arguments. */ protected Command buildCommand(RequestType requestType) { return buildCommand(requestType, buildArgs()); diff --git a/java/client/src/test/java/glide/api/RedisClientTest.java b/java/client/src/test/java/glide/api/RedisClientTest.java index 46ad50f281..8b20feb65c 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -2,6 +2,7 @@ package glide.api; import static glide.api.BaseClient.OK; +import static glide.api.commands.ServerManagementCommands.VERSION_REDIS_API; import static glide.api.commands.SortedSetBaseCommands.WITH_SCORES_REDIS_API; import static glide.api.commands.SortedSetBaseCommands.WITH_SCORE_REDIS_API; import static glide.api.models.commands.LInsertOptions.InsertPosition.BEFORE; @@ -62,6 +63,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.Info; import static redis_request.RedisRequestOuterClass.RequestType.LInsert; import static redis_request.RedisRequestOuterClass.RequestType.LLen; +import static redis_request.RedisRequestOuterClass.RequestType.LOLWUT; import static redis_request.RedisRequestOuterClass.RequestType.LPop; import static redis_request.RedisRequestOuterClass.RequestType.LPush; import static redis_request.RedisRequestOuterClass.RequestType.LPushX; @@ -73,6 +75,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.MGet; import static redis_request.RedisRequestOuterClass.RequestType.MSet; import static redis_request.RedisRequestOuterClass.RequestType.ObjectEncoding; +import static redis_request.RedisRequestOuterClass.RequestType.ObjectRefcount; import static redis_request.RedisRequestOuterClass.RequestType.PExpire; import static redis_request.RedisRequestOuterClass.RequestType.PExpireAt; import static redis_request.RedisRequestOuterClass.RequestType.PTTL; @@ -86,6 +89,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.RPushX; import static redis_request.RedisRequestOuterClass.RequestType.SAdd; import static redis_request.RedisRequestOuterClass.RequestType.SCard; +import static redis_request.RedisRequestOuterClass.RequestType.SDiff; import static redis_request.RedisRequestOuterClass.RequestType.SDiffStore; import static redis_request.RedisRequestOuterClass.RequestType.SInter; import static redis_request.RedisRequestOuterClass.RequestType.SInterStore; @@ -1739,6 +1743,29 @@ public void scard_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void sdiff_returns_success() { + // setup + String[] keys = new String[] {"key1", "key2"}; + Set value = Set.of("1", "2"); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand(eq(SDiff), eq(keys), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.sdiff(keys); + Set payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void smismember_returns_success() { @@ -3062,6 +3089,91 @@ public void lastsave_returns_success() { assertEquals(value, response.get()); } + @SneakyThrows + @Test + public void lolwut_returns_success() { + // setup + String value = "pewpew"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LOLWUT), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lolwut(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, response.get()); + } + + @SneakyThrows + @Test + public void lolwut_with_params_returns_success() { + // setup + String value = "pewpew"; + String[] arguments = new String[] {"1", "2"}; + int[] params = new int[] {1, 2}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LOLWUT), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lolwut(params); + + // verify + assertEquals(testResponse, response); + assertEquals(value, response.get()); + } + + @SneakyThrows + @Test + public void lolwut_with_version_returns_success() { + // setup + String value = "pewpew"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(LOLWUT), eq(new String[] {VERSION_REDIS_API, "42"}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lolwut(42); + + // verify + assertEquals(testResponse, response); + assertEquals(value, response.get()); + } + + @SneakyThrows + @Test + public void lolwut_with_version_and_params_returns_success() { + // setup + String value = "pewpew"; + String[] arguments = new String[] {VERSION_REDIS_API, "42", "1", "2"}; + int[] params = new int[] {1, 2}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LOLWUT), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lolwut(42, params); + + // verify + assertEquals(testResponse, response); + assertEquals(value, response.get()); + } + @SneakyThrows @Test public void linsert_returns_success() { @@ -3282,4 +3394,26 @@ public void objectEncoding_returns_success() { assertEquals(testResponse, response); assertEquals(encoding, payload); } + + @SneakyThrows + @Test + public void objectRefcount_returns_success() { + // setup + String key = "testKey"; + Long refcount = 0L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(refcount); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ObjectRefcount), eq(new String[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.objectRefcount(key); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(refcount, payload); + } } diff --git a/java/client/src/test/java/glide/api/RedisClusterClientTest.java b/java/client/src/test/java/glide/api/RedisClusterClientTest.java index 89ef402f0a..92f94267d3 100644 --- a/java/client/src/test/java/glide/api/RedisClusterClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClusterClientTest.java @@ -2,6 +2,7 @@ package glide.api; import static glide.api.BaseClient.OK; +import static glide.api.commands.ServerManagementCommands.VERSION_REDIS_API; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleMultiNodeRoute.ALL_NODES; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleMultiNodeRoute.ALL_PRIMARIES; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleSingleNodeRoute.RANDOM; @@ -21,6 +22,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.ConfigSet; import static redis_request.RedisRequestOuterClass.RequestType.Echo; import static redis_request.RedisRequestOuterClass.RequestType.Info; +import static redis_request.RedisRequestOuterClass.RequestType.LOLWUT; import static redis_request.RedisRequestOuterClass.RequestType.LastSave; import static redis_request.RedisRequestOuterClass.RequestType.Ping; import static redis_request.RedisRequestOuterClass.RequestType.Time; @@ -813,4 +815,177 @@ public void lastsave_returns_with_route_success() { assertEquals(testResponse, response); assertEquals(value, response.get().getSingleValue()); } + + @SneakyThrows + @Test + public void lolwut_returns_success() { + // setup + String value = "pewpew"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LOLWUT), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lolwut(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, response.get()); + } + + @SneakyThrows + @Test + public void lolwut_with_params_returns_success() { + // setup + String value = "pewpew"; + String[] arguments = new String[] {"1", "2"}; + int[] params = new int[] {1, 2}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LOLWUT), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lolwut(params); + + // verify + assertEquals(testResponse, response); + assertEquals(value, response.get()); + } + + @SneakyThrows + @Test + public void lolwut_with_version_returns_success() { + // setup + String value = "pewpew"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(LOLWUT), eq(new String[] {VERSION_REDIS_API, "42"}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lolwut(42); + + // verify + assertEquals(testResponse, response); + assertEquals(value, response.get()); + } + + @SneakyThrows + @Test + public void lolwut_with_version_and_params_returns_success() { + // setup + String value = "pewpew"; + String[] arguments = new String[] {VERSION_REDIS_API, "42", "1", "2"}; + int[] params = new int[] {1, 2}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LOLWUT), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lolwut(42, params); + + // verify + assertEquals(testResponse, response); + assertEquals(value, response.get()); + } + + @SneakyThrows + @Test + public void lolwut_with_route_returns_success() { + // setup + ClusterValue value = ClusterValue.ofSingleValue("pewpew"); + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(LOLWUT), eq(new String[0]), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.lolwut(RANDOM); + + // verify + assertEquals(testResponse, response); + assertEquals(value, response.get()); + } + + @SneakyThrows + @Test + public void lolwut_with_params_and_route_returns_success() { + // setup + ClusterValue value = ClusterValue.ofSingleValue("pewpew"); + String[] arguments = new String[] {"1", "2"}; + int[] params = new int[] {1, 2}; + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(LOLWUT), eq(arguments), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.lolwut(params, RANDOM); + + // verify + assertEquals(testResponse, response); + assertEquals(value, response.get()); + } + + @SneakyThrows + @Test + public void lolwut_with_version_and_route_returns_success() { + // setup + ClusterValue value = ClusterValue.ofSingleValue("pewpew"); + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(LOLWUT), eq(new String[] {VERSION_REDIS_API, "42"}), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.lolwut(42, RANDOM); + + // verify + assertEquals(testResponse, response); + assertEquals(value, response.get()); + } + + @SneakyThrows + @Test + public void lolwut_with_version_and_params_and_route_returns_success() { + // setup + ClusterValue value = ClusterValue.ofSingleValue("pewpew"); + String[] arguments = new String[] {VERSION_REDIS_API, "42", "1", "2"}; + int[] params = new int[] {1, 2}; + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(LOLWUT), eq(arguments), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.lolwut(42, params, RANDOM); + + // verify + assertEquals(testResponse, response); + assertEquals(value, response.get()); + } } diff --git a/java/client/src/test/java/glide/api/models/TransactionTests.java b/java/client/src/test/java/glide/api/models/TransactionTests.java index 63adc075b8..9ded1567ba 100644 --- a/java/client/src/test/java/glide/api/models/TransactionTests.java +++ b/java/client/src/test/java/glide/api/models/TransactionTests.java @@ -1,6 +1,7 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models; +import static glide.api.commands.ServerManagementCommands.VERSION_REDIS_API; import static glide.api.commands.SortedSetBaseCommands.WITH_SCORES_REDIS_API; import static glide.api.commands.SortedSetBaseCommands.WITH_SCORE_REDIS_API; import static glide.api.models.commands.ExpireOptions.HAS_EXISTING_EXPIRY; @@ -47,6 +48,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.Info; import static redis_request.RedisRequestOuterClass.RequestType.LInsert; import static redis_request.RedisRequestOuterClass.RequestType.LLen; +import static redis_request.RedisRequestOuterClass.RequestType.LOLWUT; import static redis_request.RedisRequestOuterClass.RequestType.LPop; import static redis_request.RedisRequestOuterClass.RequestType.LPush; import static redis_request.RedisRequestOuterClass.RequestType.LPushX; @@ -58,6 +60,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.MGet; import static redis_request.RedisRequestOuterClass.RequestType.MSet; import static redis_request.RedisRequestOuterClass.RequestType.ObjectEncoding; +import static redis_request.RedisRequestOuterClass.RequestType.ObjectRefcount; import static redis_request.RedisRequestOuterClass.RequestType.PExpire; import static redis_request.RedisRequestOuterClass.RequestType.PExpireAt; import static redis_request.RedisRequestOuterClass.RequestType.PTTL; @@ -71,6 +74,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.RPushX; import static redis_request.RedisRequestOuterClass.RequestType.SAdd; import static redis_request.RedisRequestOuterClass.RequestType.SCard; +import static redis_request.RedisRequestOuterClass.RequestType.SDiff; import static redis_request.RedisRequestOuterClass.RequestType.SDiffStore; import static redis_request.RedisRequestOuterClass.RequestType.SInter; import static redis_request.RedisRequestOuterClass.RequestType.SInterStore; @@ -456,6 +460,12 @@ InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3, false), new Limit(1, 2)), transaction.lastsave(); results.add(Pair.of(LastSave, buildArgs())); + transaction.lolwut().lolwut(5).lolwut(new int[] {1, 2}).lolwut(6, new int[] {42}); + results.add(Pair.of(LOLWUT, buildArgs())); + results.add(Pair.of(LOLWUT, buildArgs(VERSION_REDIS_API, "5"))); + results.add(Pair.of(LOLWUT, buildArgs("1", "2"))); + results.add(Pair.of(LOLWUT, buildArgs(VERSION_REDIS_API, "6", "42"))); + transaction.persist("key"); results.add(Pair.of(Persist, buildArgs("key"))); @@ -503,6 +513,9 @@ InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3, false), new Limit(1, 2)), PfMerge, ArgsArray.newBuilder().addArgs("hll").addArgs("hll1").addArgs("hll2").build())); + transaction.sdiff(new String[] {"key1", "key2"}); + results.add(Pair.of(SDiff, buildArgs("key1", "key2"))); + transaction.sdiffstore("key1", new String[] {"key2", "key3"}); results.add( Pair.of( @@ -512,6 +525,9 @@ InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3, false), new Limit(1, 2)), transaction.objectEncoding("key"); results.add(Pair.of(ObjectEncoding, buildArgs("key"))); + transaction.objectRefcount("key"); + results.add(Pair.of(ObjectRefcount, buildArgs("key"))); + var protobufTransaction = transaction.getProtobufTransaction().build(); for (int idx = 0; idx < protobufTransaction.getCommandsCount(); idx++) { diff --git a/java/integTest/src/test/java/glide/SharedCommandTests.java b/java/integTest/src/test/java/glide/SharedCommandTests.java index 9b1a36ecc0..2d7e83fc99 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -1041,6 +1041,43 @@ public void sinterstore(BaseClient client) { assertTrue(executionException.getCause() instanceof RequestException); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void sdiff(BaseClient client) { + String key1 = "{key}-1-" + UUID.randomUUID(); + String key2 = "{key}-2-" + UUID.randomUUID(); + String key3 = "{key}-3-" + UUID.randomUUID(); + + assertEquals(3, client.sadd(key1, new String[] {"a", "b", "c"}).get()); + assertEquals(3, client.sadd(key2, new String[] {"c", "d", "e"}).get()); + + assertEquals(Set.of("a", "b"), client.sdiff(new String[] {key1, key2}).get()); + assertEquals(Set.of("d", "e"), client.sdiff(new String[] {key2, key1}).get()); + + // second set is empty + assertEquals(Set.of("a", "b", "c"), client.sdiff(new String[] {key1, key3}).get()); + + // first set is empty + assertEquals(Set.of(), client.sdiff(new String[] {key3, key1}).get()); + + // source key exists, but it is not a set + assertEquals(OK, client.set(key3, "value").get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.sdiff(new String[] {key1, key3}).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // same-slot requirement + if (client instanceof RedisClusterClient) { + executionException = + assertThrows( + ExecutionException.class, + () -> client.sdiff(new String[] {"abc", "zxy", "lkn"}).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue(executionException.getMessage().toLowerCase().contains("crossslot")); + } + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -2715,4 +2752,21 @@ public void objectEncoding_returns_stream(BaseClient client) { assertNotNull(client.xadd(streamKey, Map.of("field", "value"))); assertEquals("stream", client.objectEncoding(streamKey).get()); } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void objectRefcount_returns_null(BaseClient client) { + String nonExistingKey = UUID.randomUUID().toString(); + assertNull(client.objectRefcount(nonExistingKey).get()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void objectRefcount(BaseClient client) { + String key = UUID.randomUUID().toString(); + assertEquals(OK, client.set(key, "").get()); + assertTrue(client.objectRefcount(key).get() >= 0L); + } } diff --git a/java/integTest/src/test/java/glide/TransactionTestUtilities.java b/java/integTest/src/test/java/glide/TransactionTestUtilities.java index c2ce0f9a7f..2cea0849e0 100644 --- a/java/integTest/src/test/java/glide/TransactionTestUtilities.java +++ b/java/integTest/src/test/java/glide/TransactionTestUtilities.java @@ -1,6 +1,7 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide; +import static glide.TestConfiguration.REDIS_VERSION; import static glide.api.BaseClient.OK; import static glide.api.models.commands.LInsertOptions.InsertPosition.AFTER; @@ -113,6 +114,7 @@ public static BaseTransaction transactionTest(BaseTransaction baseTransact baseTransaction.sunionstore(setKey3, new String[] {setKey2, key7}); baseTransaction.sdiffstore(setKey3, new String[] {setKey2, key7}); baseTransaction.sinterstore(setKey3, new String[] {setKey2, key7}); + baseTransaction.sdiff(new String[] {setKey2, setKey3}); baseTransaction.smove(key7, setKey2, "baz"); baseTransaction.zadd(key8, Map.of("one", 1.0, "two", 2.0, "three", 3.0)); @@ -152,6 +154,8 @@ public static BaseTransaction transactionTest(BaseTransaction baseTransact baseTransaction.echo("GLIDE"); + baseTransaction.lolwut(1); + baseTransaction.rpushx(listKey3, new String[] {"_"}).lpushx(listKey3, new String[] {"_"}); baseTransaction .lpush(listKey3, new String[] {value1, value2, value3}) @@ -226,6 +230,7 @@ public static Object[] transactionTestResult() { 3L, // sunionstore(setKey3, new String[] { setKey2, key7 }) 2L, // sdiffstore(setKey3, new String[] { setKey2, key7 }) 0L, // sinterstore(setKey3, new String[] { setKey2, key7 }) + Set.of("a", "b"), // sdiff(new String[] {setKey2, setKey3}) true, // smove(key7, setKey2, "baz") 3L, 0L, // zrank(key8, "one") @@ -258,6 +263,7 @@ public static Object[] transactionTestResult() { Map.of("timeout", "1000"), OK, "GLIDE", // echo + "Redis ver. " + REDIS_VERSION + '\n', // lolwut(1) 0L, // rpushx(listKey3, new String[] { "_" }) 0L, // lpushx(listKey3, new String[] { "_" }) 3L, // lpush(listKey3, new String[] { value1, value2, value3}) diff --git a/java/integTest/src/test/java/glide/cluster/ClusterTransactionTests.java b/java/integTest/src/test/java/glide/cluster/ClusterTransactionTests.java index 498893466f..13a8a0ab3c 100644 --- a/java/integTest/src/test/java/glide/cluster/ClusterTransactionTests.java +++ b/java/integTest/src/test/java/glide/cluster/ClusterTransactionTests.java @@ -92,4 +92,17 @@ public void lastsave() { var response = clusterClient.exec(new ClusterTransaction().lastsave()).get(); assertTrue(Instant.ofEpochSecond((long) response[0]).isAfter(yesterday)); } + + // TODO: Enable when https://github.com/amazon-contributing/redis-rs/pull/138 is merged. + // @Test + // @SneakyThrows + // public void objectRefcount() { + // String objectRefcountKey = "key"; + // ClusterTransaction transaction = new ClusterTransaction(); + // transaction.set(objectRefcountKey, ""); + // transaction.objectRefcount(objectRefcountKey); + // var response = clusterClient.exec(transaction).get(); + // assertEquals(OK, response[0]); + // assertTrue((long) response[1] >= 0L); + // } } diff --git a/java/integTest/src/test/java/glide/cluster/CommandTests.java b/java/integTest/src/test/java/glide/cluster/CommandTests.java index 81dd2586be..0769ad0a34 100644 --- a/java/integTest/src/test/java/glide/cluster/CommandTests.java +++ b/java/integTest/src/test/java/glide/cluster/CommandTests.java @@ -5,6 +5,7 @@ import static glide.TestConfiguration.REDIS_VERSION; import static glide.TestUtilities.getFirstEntryFromMultiValue; import static glide.TestUtilities.getValueFromInfo; +import static glide.TestUtilities.parseInfoResponseToMap; import static glide.api.BaseClient.OK; import static glide.api.models.commands.InfoOptions.Section.CLIENTS; import static glide.api.models.commands.InfoOptions.Section.CLUSTER; @@ -13,6 +14,7 @@ import static glide.api.models.commands.InfoOptions.Section.EVERYTHING; import static glide.api.models.commands.InfoOptions.Section.MEMORY; import static glide.api.models.commands.InfoOptions.Section.REPLICATION; +import static glide.api.models.commands.InfoOptions.Section.SERVER; import static glide.api.models.commands.InfoOptions.Section.STATS; import static glide.api.models.configuration.RequestRoutingConfiguration.ByAddressRoute; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleMultiNodeRoute.ALL_NODES; @@ -368,10 +370,16 @@ public void config_reset_stat() { @Test @SneakyThrows public void config_rewrite_non_existent_config_file() { - // The setup for the Integration Tests server does not include a configuration file for Redis. - ExecutionException executionException = - assertThrows(ExecutionException.class, () -> clusterClient.configRewrite().get()); - assertTrue(executionException.getCause() instanceof RequestException); + var info = clusterClient.info(InfoOptions.builder().section(SERVER).build(), RANDOM).get(); + var configFile = parseInfoResponseToMap(info.getSingleValue()).get("config_file"); + + if (configFile.isEmpty()) { + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> clusterClient.configRewrite().get()); + assertTrue(executionException.getCause() instanceof RequestException); + } else { + assertEquals(OK, clusterClient.configRewrite().get()); + } } // returns the line that contains the word "myself", up to that point. This is done because the @@ -571,4 +579,41 @@ public void lastsave() { assertTrue(Instant.ofEpochSecond(value).isAfter(yesterday)); } } + + @Test + @SneakyThrows + public void lolwut_lolwut() { + var response = clusterClient.lolwut().get(); + System.out.printf("%nLOLWUT cluster client standard response%n%s%n", response); + assertTrue(response.contains("Redis ver. " + REDIS_VERSION)); + + response = clusterClient.lolwut(new int[] {50, 20}).get(); + System.out.printf( + "%nLOLWUT cluster client standard response with params 50 20%n%s%n", response); + assertTrue(response.contains("Redis ver. " + REDIS_VERSION)); + + response = clusterClient.lolwut(6).get(); + System.out.printf("%nLOLWUT cluster client ver 6 response%n%s%n", response); + assertTrue(response.contains("Redis ver. " + REDIS_VERSION)); + + response = clusterClient.lolwut(5, new int[] {30, 4, 4}).get(); + System.out.printf("%nLOLWUT cluster client ver 5 response with params 30 4 4%n%s%n", response); + assertTrue(response.contains("Redis ver. " + REDIS_VERSION)); + + var clusterResponse = clusterClient.lolwut(ALL_NODES).get(); + for (var nodeResponse : clusterResponse.getMultiValue().values()) { + assertTrue(nodeResponse.contains("Redis ver. " + REDIS_VERSION)); + } + + clusterResponse = clusterClient.lolwut(new int[] {10, 20}, ALL_NODES).get(); + for (var nodeResponse : clusterResponse.getMultiValue().values()) { + assertTrue(nodeResponse.contains("Redis ver. " + REDIS_VERSION)); + } + + clusterResponse = clusterClient.lolwut(2, RANDOM).get(); + assertTrue(clusterResponse.getSingleValue().contains("Redis ver. " + REDIS_VERSION)); + + clusterResponse = clusterClient.lolwut(2, new int[] {10, 20}, RANDOM).get(); + assertTrue(clusterResponse.getSingleValue().contains("Redis ver. " + REDIS_VERSION)); + } } diff --git a/java/integTest/src/test/java/glide/standalone/CommandTests.java b/java/integTest/src/test/java/glide/standalone/CommandTests.java index 16343a2560..1c311cd1b7 100644 --- a/java/integTest/src/test/java/glide/standalone/CommandTests.java +++ b/java/integTest/src/test/java/glide/standalone/CommandTests.java @@ -4,11 +4,13 @@ import static glide.TestConfiguration.REDIS_VERSION; import static glide.TestConfiguration.STANDALONE_PORTS; import static glide.TestUtilities.getValueFromInfo; +import static glide.TestUtilities.parseInfoResponseToMap; import static glide.api.BaseClient.OK; import static glide.api.models.commands.InfoOptions.Section.CLUSTER; import static glide.api.models.commands.InfoOptions.Section.CPU; import static glide.api.models.commands.InfoOptions.Section.EVERYTHING; import static glide.api.models.commands.InfoOptions.Section.MEMORY; +import static glide.api.models.commands.InfoOptions.Section.SERVER; import static glide.api.models.commands.InfoOptions.Section.STATS; import static glide.cluster.CommandTests.DEFAULT_INFO_SECTIONS; import static glide.cluster.CommandTests.EVERYTHING_INFO_SECTIONS; @@ -185,10 +187,16 @@ public void config_reset_stat() { @Test @SneakyThrows public void config_rewrite_non_existent_config_file() { - // The setup for the Integration Tests server does not include a configuration file for Redis. - ExecutionException executionException = - assertThrows(ExecutionException.class, () -> regularClient.configRewrite().get()); - assertTrue(executionException.getCause() instanceof RequestException); + var info = regularClient.info(InfoOptions.builder().section(SERVER).build()).get(); + var configFile = parseInfoResponseToMap(info).get("config_file"); + + if (configFile.isEmpty()) { + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> regularClient.configRewrite().get()); + assertTrue(executionException.getCause() instanceof RequestException); + } else { + assertEquals(OK, regularClient.configRewrite().get()); + } } @Test @@ -275,4 +283,26 @@ public void lastsave() { var yesterday = Instant.now().minus(1, ChronoUnit.DAYS); assertTrue(Instant.ofEpochSecond(result).isAfter(yesterday)); } + + @Test + @SneakyThrows + public void lolwut_lolwut() { + var response = regularClient.lolwut().get(); + System.out.printf("%nLOLWUT standalone client standard response%n%s%n", response); + assertTrue(response.contains("Redis ver. " + REDIS_VERSION)); + + response = regularClient.lolwut(new int[] {30, 4, 4}).get(); + System.out.printf( + "%nLOLWUT standalone client standard response with params 30 4 4%n%s%n", response); + assertTrue(response.contains("Redis ver. " + REDIS_VERSION)); + + response = regularClient.lolwut(5).get(); + System.out.printf("%nLOLWUT standalone client ver 5 response%n%s%n", response); + assertTrue(response.contains("Redis ver. " + REDIS_VERSION)); + + response = regularClient.lolwut(6, new int[] {50, 20}).get(); + System.out.printf( + "%nLOLWUT standalone client ver 6 response with params 50 20%n%s%n", response); + assertTrue(response.contains("Redis ver. " + REDIS_VERSION)); + } } diff --git a/java/integTest/src/test/java/glide/standalone/TransactionTests.java b/java/integTest/src/test/java/glide/standalone/TransactionTests.java index 532adb7703..54a03488a3 100644 --- a/java/integTest/src/test/java/glide/standalone/TransactionTests.java +++ b/java/integTest/src/test/java/glide/standalone/TransactionTests.java @@ -121,4 +121,16 @@ public void lastsave() { var response = client.exec(new Transaction().lastsave()).get(); assertTrue(Instant.ofEpochSecond((long) response[0]).isAfter(yesterday)); } + + @Test + @SneakyThrows + public void objectRefcount() { + String objectRefcountKey = "key"; + Transaction transaction = new Transaction(); + transaction.set(objectRefcountKey, ""); + transaction.objectRefcount(objectRefcountKey); + var response = client.exec(transaction).get(); + assertEquals(OK, response[0]); + assertTrue((long) response[1] >= 0L); + } } diff --git a/node/.prettierignore b/node/.prettierignore new file mode 100644 index 0000000000..086fea31e1 --- /dev/null +++ b/node/.prettierignore @@ -0,0 +1,5 @@ +# ignore that dir, because there are a lot of files which we don't manage, e.g. json files in cargo crates +rust-client/* +# unignore specific files +!rust-client/package.json +!rust-client/tsconfig.json diff --git a/python/python/tests/test_async_client.py b/python/python/tests/test_async_client.py index eb23589e08..ea61122240 100644 --- a/python/python/tests/test_async_client.py +++ b/python/python/tests/test_async_client.py @@ -2184,20 +2184,10 @@ def clean_result(value: TResult): async def test_cluster_fail_routing_by_address_if_no_port_is_provided( self, redis_client: RedisClusterClient ): - with pytest.raises(RequestError) as e: + with pytest.raises(RequestError): await redis_client.info(route=ByAddressRoute("foo")) -@pytest.mark.asyncio -class TestExceptions: - @pytest.mark.parametrize("cluster_mode", [True, False]) - @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_timeout_exception_with_blpop(self, redis_client: TRedisClient): - key = get_random_string(10) - with pytest.raises(TimeoutError): - await redis_client.custom_command(["BLPOP", key, "1"]) - - @pytest.mark.asyncio class TestScripts: @pytest.mark.smoke_test