Skip to content

Commit ed9147f

Browse files
committed
add flexible scan method usable with a while loop and encapsulate callback, handle reaming review comments
1 parent 90f514c commit ed9147f

File tree

3 files changed

+103
-47
lines changed

3 files changed

+103
-47
lines changed

examples/scan_keys.rs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
// The example implements three commands:
44
//
55
// 1. `scan_keys` - scans all keys in the database and returns their names as an array of RedisString.
6-
// 2. `scan_key <key>` - scans all fields and values in a hash key using a closure that stores the field/value pairs as an array of RedisString.
6+
// 2. `scan_key <key>` - scans all fields by using a closure and a while loop, thus allowing an early stop. Don't use the early stop but collects all the field/value pairs as an array of RedisString.
7+
// 3. `scan_key_foreach <key>` - scans all fields and values in a hash key using a closure that stores the field/value pairs as an array of RedisString.
78

89
use redis_module::{
910
key::{KeyFlags, RedisKey}, redis_module, Context, KeysCursor, RedisError, RedisResult, RedisString, RedisValue, ScanKeyCursor
@@ -24,6 +25,27 @@ fn scan_keys(ctx: &Context, _args: Vec<RedisString>) -> RedisResult {
2425
Ok(RedisValue::Array(res))
2526
}
2627

28+
fn scan_key(ctx: &Context, args: Vec<RedisString>) -> RedisResult {
29+
// only argument is the key name
30+
if args.len() != 2 {
31+
return Err(RedisError::WrongArity);
32+
}
33+
34+
let key_name = &args[1];
35+
let key = ctx.open_key_with_flags(key_name, KeyFlags::NOEFFECTS | KeyFlags::NOEXPIRE | KeyFlags::ACCESS_EXPIRED );
36+
let cursor = ScanKeyCursor::new(key);
37+
38+
let mut res = Vec::new();
39+
while cursor.scan(|_key, field, value| {
40+
res.push(RedisValue::BulkRedisString(field.clone()));
41+
res.push(RedisValue::BulkRedisString(value.clone()));
42+
}) {
43+
// here we could do something between scans if needed, like an early stop
44+
}
45+
46+
Ok(RedisValue::Array(res))
47+
}
48+
2749
/// Scans all fields and values in a hash key and returns them as an array of RedisString.
2850
/// The command takes one argument: the name of the hash key to scan.
2951
fn scan_key_foreach(ctx: &Context, args: Vec<RedisString>) -> RedisResult {
@@ -55,6 +77,7 @@ redis_module! {
5577
data_types: [],
5678
commands: [
5779
["scan_keys", scan_keys, "readonly", 0, 0, 0, ""],
58-
["scan_key", scan_key_foreach, "readonly", 0, 0, 0, ""],
80+
["scan_key", scan_key, "readonly", 0, 0, 0, ""],
81+
["scan_key_foreach", scan_key_foreach, "readonly", 0, 0, 0, ""],
5982
],
6083
}

src/context/key_cursor.rs

Lines changed: 59 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,19 @@ use crate::{key::RedisKey, raw, RedisString};
77

88
/// A cursor to scan field/value pairs of a (hash) key.
99
///
10-
/// This is a wrapper around the [RedisModule_ScanKey](https://redis.io/docs/latest/develop/reference/modules/modules-api-ref/#redismodule_scankey)
11-
/// function from the C API. It provides access via a closure given to [`ScanKeyCursor::foreach`]
12-
///
10+
/// It provides access via a closure given to [`ScanKeyCursor::foreach`] or if you need more control, you can use [`ScanKeyCursor::scan`]
11+
/// and implement your own loop, e.g. to allow an early stop.
12+
///
1313
/// ## Example usage
14-
///
14+
///
1515
/// Here we show how to extract values to communicate them back to the Redis client. We assume that the following hash key is setup before:
16-
///
16+
///
1717
/// ```text
1818
/// HSET user:123 name Alice age 29 location Austin
1919
/// ```
20-
///
20+
///
2121
/// The following example command implementation scans all fields and values in the hash key and returns them as an array of RedisString.
22-
///
22+
///
2323
/// ```ignore
2424
/// fn example_scan_key_foreach(ctx: &Context) -> RedisResult {
2525
/// let key = ctx.open_key_with_flags("user:123", KeyFlags::NOEFFECTS | KeyFlags::NOEXPIRE | KeyFlags::ACCESS_EXPIRED );
@@ -35,9 +35,9 @@ use crate::{key::RedisKey, raw, RedisString};
3535
/// Ok(RedisValue::Array(res.take()))
3636
/// }
3737
/// ```
38-
///
38+
///
3939
/// The method will produce the following output:
40-
///
40+
///
4141
/// ```text
4242
/// 1) "name"
4343
/// 2) "Alice"
@@ -63,22 +63,31 @@ impl ScanKeyCursor {
6363
unsafe { raw::RedisModule_ScanCursorRestart.unwrap()(self.inner_cursor) };
6464
}
6565

66-
/// Implements a callback based foreach loop over all fields and values in the hash key, use that for optimal performance.
67-
pub fn foreach<F: FnMut(&RedisKey, &RedisString, &RedisString)>(&self, f: F) {
68-
// Safety: Assumption: c-side initialized the function ptr and it is is never changed,
66+
pub fn scan<F: FnMut(&RedisKey, &RedisString, &RedisString)>(&self, f: F) -> bool {
67+
// the following is the callback definition
68+
// foreach `ScanKey` call the callback may be called multiple times
69+
use pimpl::scan_callback;
70+
71+
// Safety: The c-side initialized the function ptr and it is is never changed,
6972
// i.e. after module initialization the function pointers stay valid till the end of the program.
70-
let scan_key = unsafe { raw::RedisModule_ScanKey.unwrap() };
73+
let res = unsafe {
74+
raw::RedisModule_ScanKey.unwrap()(
75+
self.key.key_inner,
76+
self.inner_cursor,
77+
Some(scan_callback::<F>),
78+
&f as *const F as *mut c_void,
79+
)
80+
};
81+
82+
res != 0
83+
}
7184

72-
let mut res = 1;
73-
while res != 0 {
74-
res = unsafe {
75-
scan_key(
76-
self.key.key_inner,
77-
self.inner_cursor,
78-
Some(foreach_callback::<F>),
79-
&f as *const F as *mut c_void,
80-
)
81-
}
85+
/// Implements a callback based foreach loop over all fields and values in the hash key, use that for optimal performance.
86+
pub fn foreach<F: FnMut(&RedisKey, &RedisString, &RedisString)>(&self, mut f: F) {
87+
// the following is the callback definition
88+
// foreach `ScanKey` call the callback may be called multiple times
89+
while self.scan(&mut f) {
90+
// do nothing, the callback does the work
8291
}
8392
}
8493
}
@@ -89,28 +98,35 @@ impl Drop for ScanKeyCursor {
8998
}
9099
}
91100

92-
/// The callback that is used by [`ScanKeyCursor::foreach`] as argument to `RedisModule_ScanKey`.
93-
///
94-
/// The `data` pointer is the closure given to [`ScanKeyCursor::foreach`] and the callback forwards
95-
/// references to the key, field and value to that closure.
96-
unsafe extern "C" fn foreach_callback<F: FnMut(&RedisKey, &RedisString, &RedisString)>(
97-
key: *mut raw::RedisModuleKey,
98-
field: *mut raw::RedisModuleString,
99-
value: *mut raw::RedisModuleString,
100-
data: *mut c_void,
101-
) {
102-
let ctx = ptr::null_mut();
103-
let key = RedisKey::from_raw_parts(ctx, key);
101+
// the module contains the private implementation details of the cursor.
102+
mod pimpl {
103+
use super::*;
104+
105+
/// The callback that is used by [`ScanKeyCursor::scan`] and [`ScanKeyCursor::foreach`] as argument to `RedisModule_ScanKey`.
106+
///
107+
/// The `data` pointer is the closure given to [`ScanKeyCursor::foreach`] and the callback forwards
108+
/// references to the key, field and value to that closure.
109+
pub(super) unsafe extern "C" fn scan_callback<
110+
F: FnMut(&RedisKey, &RedisString, &RedisString),
111+
>(
112+
key: *mut raw::RedisModuleKey,
113+
field: *mut raw::RedisModuleString,
114+
value: *mut raw::RedisModuleString,
115+
data: *mut c_void,
116+
) {
117+
let ctx = ptr::null_mut();
118+
let key = RedisKey::from_raw_parts(ctx, key);
104119

105-
let field = RedisString::from_redis_module_string(ctx, field);
106-
let value = RedisString::from_redis_module_string(ctx, value);
120+
let field = RedisString::from_redis_module_string(ctx, field);
121+
let value = RedisString::from_redis_module_string(ctx, value);
107122

108-
let callback = unsafe { &mut *(data.cast::<F>()) };
109-
callback(&key, &field, &value);
123+
let callback = unsafe { &mut *(data.cast::<F>()) };
124+
callback(&key, &field, &value);
110125

111-
// we're not the owner of field and value strings
112-
field.take();
113-
value.take();
126+
// we're not the owner of field and value strings
127+
field.take();
128+
value.take();
114129

115-
key.take(); // we're not the owner of the key either
130+
key.take(); // we're not the owner of the key either
131+
}
116132
}

tests/integration.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ fn test_scan() -> Result<()> {
201201
}
202202

203203
#[test]
204-
fn test_scan_key_foreach() -> Result<()> {
204+
fn test_scan_key() -> Result<()> {
205205
let mut con = TestConnection::new("scan_keys");
206206
redis::cmd("hset")
207207
.arg(&[
@@ -210,7 +210,24 @@ fn test_scan_key_foreach() -> Result<()> {
210210
.query::<()>(&mut con)
211211
.with_context(|| "failed to hset")?;
212212

213-
let res: Vec<String> = redis::cmd("scan_key_fe")
213+
let res: Vec<String> = redis::cmd("scan_key")
214+
.arg(&["user:123"])
215+
.query(&mut con)?;
216+
assert_eq!(&res, &["name", "Alice", "age", "29", "location", "Austin"]);
217+
Ok(())
218+
}
219+
220+
#[test]
221+
fn test_scan_key_for_each() -> Result<()> {
222+
let mut con = TestConnection::new("scan_keys");
223+
redis::cmd("hset")
224+
.arg(&[
225+
"user:123", "name", "Alice", "age", "29", "location", "Austin",
226+
])
227+
.query::<()>(&mut con)
228+
.with_context(|| "failed to hset")?;
229+
230+
let res: Vec<String> = redis::cmd("scan_key_foreach")
214231
.arg(&["user:123"])
215232
.query(&mut con)?;
216233
assert_eq!(&res, &["name", "Alice", "age", "29", "location", "Austin"]);

0 commit comments

Comments
 (0)