Skip to content

Commit a5dc2d2

Browse files
committed
Fix blocking commands tests in wrappers
1 parent 802230c commit a5dc2d2

File tree

6 files changed

+185
-12
lines changed

6 files changed

+185
-12
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
#### Fixes
1212
* Python: Fix typing error "‘type’ object is not subscriptable" ([#1203](https://github.com/aws/glide-for-redis/pull/1203))
13+
* Core: Fixed blocking commands to use the specified timeout from the command argument
1314

1415
## 0.3.3 (2024-03-28)
1516

glide-core/src/client/mod.rs

Lines changed: 137 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use futures::FutureExt;
88
use logger_core::log_info;
99
use redis::aio::ConnectionLike;
1010
use redis::cluster_async::ClusterConnection;
11-
use redis::cluster_routing::{RoutingInfo, SingleNodeRoutingInfo};
11+
use redis::cluster_routing::{Routable, RoutingInfo, SingleNodeRoutingInfo};
1212
use redis::RedisResult;
1313
use redis::{Cmd, ErrorKind, Value};
1414
pub use standalone_client::StandaloneClient;
@@ -104,14 +104,57 @@ async fn run_with_timeout<T>(
104104
.and_then(|res| res)
105105
}
106106

107+
const BLOCKING_CMD_TIMEOUT_BUFFER: f64 = 0.5; // seconds
108+
109+
enum TimeUnit {
110+
Milliseconds = 1000,
111+
Seconds = 1,
112+
}
113+
114+
// Attempts to get the timeout duration from the command argument at `timeout_idx`.
115+
// If the argument can be parsed into a duration, it returns the duration in seconds. Otherwise, it returns None.
116+
fn try_get_timeout_from_cmd_arg(
117+
cmd: &Cmd,
118+
timeout_idx: usize,
119+
time_unit: TimeUnit,
120+
) -> Option<Duration> {
121+
cmd.arg_idx(timeout_idx).and_then(|timeout_bytes| {
122+
std::str::from_utf8(timeout_bytes)
123+
.ok()
124+
.and_then(|timeout_str| {
125+
timeout_str.parse::<f64>().ok().map(|timeout| {
126+
let timeout_secs = timeout / ((time_unit as i32) as f64);
127+
// We add a buffer to the request timeout to ensure we receive a response from the server
128+
Duration::from_secs_f64(timeout_secs + BLOCKING_CMD_TIMEOUT_BUFFER)
129+
})
130+
})
131+
})
132+
}
133+
134+
fn get_request_timeout(cmd: &Cmd, default_timeout: Duration) -> Duration {
135+
let command = cmd.command().unwrap_or_default();
136+
let blocking_timeout = match command.as_slice() {
137+
b"BLPOP" | b"BRPOP" | b"BLMOVE" | b"BZPOPMAX" | b"BZPOPMIN" | b"BRPOPLPUSH" => {
138+
try_get_timeout_from_cmd_arg(cmd, cmd.args_iter().len() - 1, TimeUnit::Seconds)
139+
}
140+
b"BLMPOP" | b"BZMPOP" => try_get_timeout_from_cmd_arg(cmd, 1, TimeUnit::Seconds),
141+
b"XREAD" | b"XREADGROUP" => cmd
142+
.position(b"BLOCK")
143+
.and_then(|idx| try_get_timeout_from_cmd_arg(cmd, idx + 1, TimeUnit::Milliseconds)),
144+
_ => None,
145+
};
146+
147+
blocking_timeout.unwrap_or(default_timeout)
148+
}
149+
107150
impl Client {
108151
pub fn send_command<'a>(
109152
&'a mut self,
110153
cmd: &'a Cmd,
111154
routing: Option<RoutingInfo>,
112155
) -> redis::RedisFuture<'a, Value> {
113156
let expected_type = expected_type_for_cmd(cmd);
114-
run_with_timeout(self.request_timeout, async move {
157+
run_with_timeout(get_request_timeout(cmd, self.request_timeout), async move {
115158
match self.internal_client {
116159
ClientWrapper::Standalone(ref mut client) => client.send_command(cmd).await,
117160

@@ -472,3 +515,95 @@ impl GlideClientForTests for StandaloneClient {
472515
self.send_command(cmd).boxed()
473516
}
474517
}
518+
519+
#[cfg(test)]
520+
mod tests {
521+
use std::time::Duration;
522+
523+
use redis::Cmd;
524+
525+
use crate::client::{get_request_timeout, TimeUnit, BLOCKING_CMD_TIMEOUT_BUFFER};
526+
527+
use super::try_get_timeout_from_cmd_arg;
528+
529+
#[test]
530+
fn test_get_timeout_from_cmd_returns_correct_duration_int() {
531+
let mut cmd = Cmd::new();
532+
cmd.arg("BLPOP").arg("key1").arg("key2").arg("5");
533+
assert_eq!(
534+
try_get_timeout_from_cmd_arg(&cmd, cmd.args_iter().len() - 1, TimeUnit::Seconds),
535+
Some(Duration::from_secs_f64(5.0 + BLOCKING_CMD_TIMEOUT_BUFFER))
536+
);
537+
}
538+
539+
#[test]
540+
fn test_get_timeout_from_cmd_returns_correct_duration_float() {
541+
let mut cmd = Cmd::new();
542+
cmd.arg("BLPOP").arg("key1").arg("key2").arg(0.5);
543+
assert_eq!(
544+
try_get_timeout_from_cmd_arg(&cmd, cmd.args_iter().len() - 1, TimeUnit::Seconds),
545+
Some(Duration::from_secs_f64(0.5 + BLOCKING_CMD_TIMEOUT_BUFFER))
546+
);
547+
}
548+
549+
#[test]
550+
fn test_get_timeout_from_cmd_returns_correct_duration_milliseconds() {
551+
let mut cmd = Cmd::new();
552+
cmd.arg("XREAD").arg("BLOCK").arg("500").arg("key");
553+
assert_eq!(
554+
try_get_timeout_from_cmd_arg(&cmd, 2, TimeUnit::Milliseconds),
555+
Some(Duration::from_secs_f64(0.5 + BLOCKING_CMD_TIMEOUT_BUFFER))
556+
);
557+
}
558+
559+
#[test]
560+
fn test_get_timeout_from_cmd_returns_none_when_timeout_isnt_passed() {
561+
let mut cmd = Cmd::new();
562+
cmd.arg("BLPOP").arg("key1").arg("key2").arg("key3");
563+
assert_eq!(
564+
try_get_timeout_from_cmd_arg(&cmd, cmd.args_iter().len() - 1, TimeUnit::Seconds),
565+
None,
566+
);
567+
}
568+
569+
#[test]
570+
fn test_get_request_timeout_with_blocking_command_returns_cmd_arg_timeout() {
571+
let mut cmd = Cmd::new();
572+
cmd.arg("BLPOP").arg("key1").arg("key2").arg("500");
573+
assert_eq!(
574+
get_request_timeout(&cmd, Duration::from_millis(100)),
575+
Duration::from_secs_f64(500.0 + BLOCKING_CMD_TIMEOUT_BUFFER)
576+
);
577+
578+
let mut cmd = Cmd::new();
579+
cmd.arg("XREADGROUP").arg("BLOCK").arg("500").arg("key");
580+
assert_eq!(
581+
get_request_timeout(&cmd, Duration::from_millis(100)),
582+
Duration::from_secs_f64(0.5 + BLOCKING_CMD_TIMEOUT_BUFFER)
583+
);
584+
585+
let mut cmd = Cmd::new();
586+
cmd.arg("BLMPOP").arg("0.857").arg("key");
587+
assert_eq!(
588+
get_request_timeout(&cmd, Duration::from_millis(100)),
589+
Duration::from_secs_f64(0.857 + BLOCKING_CMD_TIMEOUT_BUFFER)
590+
);
591+
}
592+
593+
#[test]
594+
fn test_get_request_timeout_non_blocking_command_returns_default_timeout() {
595+
let mut cmd = Cmd::new();
596+
cmd.arg("SET").arg("key").arg("value").arg("PX").arg("500");
597+
assert_eq!(
598+
get_request_timeout(&cmd, Duration::from_millis(100)),
599+
Duration::from_millis(100)
600+
);
601+
602+
let mut cmd = Cmd::new();
603+
cmd.arg("XREADGROUP").arg("key");
604+
assert_eq!(
605+
get_request_timeout(&cmd, Duration::from_millis(100)),
606+
Duration::from_millis(100)
607+
);
608+
}
609+
}

glide-core/tests/test_client.rs

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -321,21 +321,58 @@ pub(crate) mod shared_client_tests {
321321
use_cluster,
322322
TestConfiguration {
323323
request_timeout: Some(1),
324-
shared_server: true,
324+
shared_server: false,
325325
..Default::default()
326326
},
327327
)
328328
.await;
329-
330329
let mut cmd = redis::Cmd::new();
331-
cmd.arg("BLPOP").arg("foo").arg(0); // 0 timeout blocks indefinitely
330+
cmd.arg("EVAL")
331+
.arg(
332+
r#"
333+
local i = 0
334+
while (true)
335+
do
336+
redis.call('ping')
337+
i = i + 1
338+
end
339+
"#,
340+
)
341+
.arg("0"); // Create a long running command to ensure we get into timeout
332342
let result = test_basics.client.send_command(&cmd, None).await;
333343
assert!(result.is_err());
334344
let err = result.unwrap_err();
335345
assert!(err.is_timeout(), "{err}");
336346
});
337347
}
338348

349+
#[rstest]
350+
#[timeout(SHORT_CLUSTER_TEST_TIMEOUT)]
351+
fn test_blocking_command_doesnt_raise_timeout_error(#[values(false, true)] use_cluster: bool) {
352+
// We test that the request timeout is based on the value specified in the blocking command argument,
353+
// and not on the one set in the client configuration. To achieve this, we execute a command designed to
354+
// be blocked until it reaches the specified command timeout. We set the client's request timeout to
355+
// a shorter duration than the blocking command's timeout. Subsequently, we confirm that we receive
356+
// a response from the server instead of encountering a timeout error.
357+
block_on_all(async {
358+
let mut test_basics = setup_test_basics(
359+
use_cluster,
360+
TestConfiguration {
361+
request_timeout: Some(1),
362+
shared_server: true,
363+
..Default::default()
364+
},
365+
)
366+
.await;
367+
368+
let mut cmd = redis::Cmd::new();
369+
cmd.arg("BLPOP").arg("foo").arg(0.3); // command should be timed out after 300 millisecond
370+
let result = test_basics.client.send_command(&cmd, None).await;
371+
assert!(result.is_ok());
372+
assert_eq!(result.unwrap(), Value::Nil);
373+
});
374+
}
375+
339376
#[rstest]
340377
#[timeout(SHORT_CLUSTER_TEST_TIMEOUT)]
341378
fn test_request_transaction_timeout(#[values(false, true)] use_cluster: bool) {

java/integTest/src/test/java/glide/SharedCommandTests.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1814,14 +1814,14 @@ public void brpop(BaseClient client) {
18141814
// nothing popped out
18151815
assertNull(
18161816
client
1817-
.brpop(new String[] {listKey2}, REDIS_VERSION.isLowerThan("7.0.0") ? 1. : 0.001)
1817+
.brpop(new String[] {listKey2}, REDIS_VERSION.isLowerThan("7.0.0") ? 1. : 0.5)
18181818
.get());
18191819

18201820
// Key exists, but it is not a list
18211821
assertEquals(OK, client.set("foo", "bar").get());
18221822
ExecutionException executionException =
18231823
assertThrows(
1824-
ExecutionException.class, () -> client.brpop(new String[] {"foo"}, .0001).get());
1824+
ExecutionException.class, () -> client.brpop(new String[] {"foo"}, 0.5).get());
18251825
assertTrue(executionException.getCause() instanceof RequestException);
18261826
}
18271827

@@ -1867,14 +1867,14 @@ public void blpop(BaseClient client) {
18671867
// nothing popped out
18681868
assertNull(
18691869
client
1870-
.blpop(new String[] {listKey2}, REDIS_VERSION.isLowerThan("7.0.0") ? 1. : 0.001)
1870+
.blpop(new String[] {listKey2}, REDIS_VERSION.isLowerThan("7.0.0") ? 1. : 0.5)
18711871
.get());
18721872

18731873
// Key exists, but it is not a list
18741874
assertEquals(OK, client.set("foo", "bar").get());
18751875
ExecutionException executionException =
18761876
assertThrows(
1877-
ExecutionException.class, () -> client.blpop(new String[] {"foo"}, .0001).get());
1877+
ExecutionException.class, () -> client.blpop(new String[] {"foo"}, 0.5).get());
18781878
assertTrue(executionException.getCause() instanceof RequestException);
18791879
}
18801880

node/tests/SharedTests.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2041,14 +2041,14 @@ export function runBaseTests<Context>(config: {
20412041
await client.rpush("brpop-test", ["foo", "bar", "baz"]),
20422042
).toEqual(3);
20432043
// Test basic usage
2044-
expect(await client.brpop(["brpop-test"], 0.1)).toEqual([
2044+
expect(await client.brpop(["brpop-test"], 0.5)).toEqual([
20452045
"brpop-test",
20462046
"baz",
20472047
]);
20482048
// Delete all values from list
20492049
expect(await client.del(["brpop-test"])).toEqual(1);
20502050
// Test null return when key doesn't exist
2051-
expect(await client.brpop(["brpop-test"], 0.1)).toEqual(null);
2051+
expect(await client.brpop(["brpop-test"], 0.5)).toEqual(null);
20522052
}, protocol);
20532053
},
20542054
config.timeout,

python/python/tests/test_async_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1945,7 +1945,7 @@ class TestExceptions:
19451945
async def test_timeout_exception_with_blpop(self, redis_client: TRedisClient):
19461946
key = get_random_string(10)
19471947
with pytest.raises(TimeoutError):
1948-
await redis_client.custom_command(["BLPOP", key, "1"])
1948+
await redis_client.custom_command(["BLPOP", key, "0.1"])
19491949

19501950

19511951
@pytest.mark.asyncio

0 commit comments

Comments
 (0)