Skip to content

Commit b76f1ac

Browse files
committed
cleanup and integration testing
1 parent be2981f commit b76f1ac

File tree

8 files changed

+80
-107
lines changed

8 files changed

+80
-107
lines changed

Cargo.toml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -147,10 +147,6 @@ redis-module = { path = "./", default-features = false }
147147
bindgen = { version = "0.66", default-features = false, features = ["logging", "prettyplease"]}
148148
cc = "1"
149149

150-
#[build-dependencies.bindgen]
151-
#version = "0.71.1"
152-
#default-features = false
153-
154150
[features]
155151
default = ["min-redis-compatibility-version-6-0", "bindgen-runtime"]
156152
min-redis-compatibility-version-7-4 = ["redis-module/min-redis-compatibility-version-7-4"]

build.rs

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,6 @@ impl ParseCallbacks for RedisModuleCallback {
1414
Some(IntKind::U64)
1515
} else if name.starts_with("REDISMODULE_REPLY") {
1616
Some(IntKind::I32)
17-
} else if name == "REDISMODULE_LIST_HEAD"
18-
|| name == "REDISMODULE_LIST_TAIL"
19-
{
20-
// These values are used as `enum` discriminants, and thus must be `isize`.
21-
Some(IntKind::Custom {
22-
name: "isize",
23-
is_signed: true,
24-
})
2517
} else if name.starts_with("REDISMODULE_NOTIFY_") {
2618
Some(IntKind::Int)
2719
} else {

examples/scan_keys.rs

Lines changed: 3 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -11,42 +11,6 @@ use std::cell::RefCell;
1111
// `scan_key_it` always copies the field and value strings, while `scan_key_fe` uses references to the field and value strings. In that example
1212
// both implementations need to clone the strings, because we want to return them as an array of RedisString.
1313
//
14-
// ## Usage examples for scan_key:
15-
//
16-
// First we need to setup a hash key with some fields and values:
17-
//
18-
// ```
19-
// HSET user:123 name Alice age 29 location Austin
20-
// ```
21-
//
22-
// We need to start redis-server with the module loaded (example for macOS, adjust path as needed):
23-
//
24-
// ```
25-
// redis-server --loadmodule ./target/debug/examples/libscan_keys.dylib
26-
// ```
27-
//
28-
// Afterwards we can call the commands via redis-cli:
29-
//
30-
// ```
31-
// > SCAN_KEYS
32-
// 1) "user:123"
33-
//
34-
// > SCAN_KEY_IT user:123
35-
// 1) "name"
36-
// 2) "Alice"
37-
// 3) "age"
38-
// 4) "29"
39-
// 5) "location"
40-
// 6) "Austin"
41-
//
42-
// > SCAN_KEY_FE user:123
43-
// 1) "name"
44-
// 2) "Alice"
45-
// 3) "age"
46-
// 4) "29"
47-
// 5) "location"
48-
// 6) "Austin"
49-
// ```
5014

5115
use redis_module::{
5216
key::{KeyFlags, RedisKey}, redis_module, Context, KeysCursor, RedisError, RedisResult, RedisString, RedisValue, ScanKeyCursor
@@ -77,16 +41,10 @@ fn scan_key_foreach(ctx: &Context, args: Vec<RedisString>) -> RedisResult {
7741

7842
let key_name = &args[1];
7943
let key = ctx.open_key_with_flags(key_name, KeyFlags::NOEFFECTS | KeyFlags::NOEXPIRE | KeyFlags::ACCESS_EXPIRED );
80-
let ty = key.key_type();
8144
let cursor = ScanKeyCursor::new(key);
82-
let out = format!("Cursor created for Scanning key: {}, type={:?}", key_name, ty);
83-
ctx.log_notice(&out);
84-
45+
8546
let res = RefCell::new(Vec::new());
8647
cursor.foreach(|_key, field, value| {
87-
let out = format!("Field: {}, Value: {}", field, value);
88-
ctx.log_notice(&out);
89-
9048
let mut res = res.borrow_mut();
9149
res.push(RedisValue::BulkRedisString(field.clone()));
9250
res.push(RedisValue::BulkRedisString(value.clone()));
@@ -106,17 +64,13 @@ fn scan_key_iterator(ctx: &Context, args: Vec<RedisString>) -> RedisResult {
10664
let key_name = &args[1];
10765
let mut res = Vec::new();
10866
let key = ctx.open_key_with_flags(key_name, KeyFlags::NOEFFECTS | KeyFlags::NOEXPIRE | KeyFlags::ACCESS_EXPIRED );
109-
let ty = key.key_type();
11067
let cursor = ScanKeyCursor::new(key);
111-
let out = format!("Cursor created for Scanning key: {}, type={:?}", key_name, ty);
112-
ctx.log_notice(&out);
113-
68+
11469
for (no, (field, value)) in cursor.iter().enumerate() {
115-
let out = format!("It: {}, Field: {}, Value: {}", no, field, value);
116-
ctx.log_notice(&out);
11770
res.push(RedisValue::BulkRedisString(field));
11871
res.push(RedisValue::BulkRedisString(value));
11972
}
73+
12074
Ok(RedisValue::Array(res))
12175
}
12276

src/context/key_scan_cursor.rs

Lines changed: 31 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,23 @@ use crate::{key::RedisKey, raw, Context, RedisString};
88
/// A cursor to scan field/value pairs of a (hash) key.
99
///
1010
/// 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 [`ScanKeyCursor::foreach`] and provides a Rust iterator via [`ScanKeyCursor::iter`].
11+
/// function from the C API. It provides access via a closure given to [`ScanKeyCursor::foreach`] and alternatively
12+
/// provides a Rust iterator via [`ScanKeyCursor::iter`].
1213
///
13-
/// Use the former if the operation can be performed in the callback, as it is more efficient. Use the latter if you need to collect the results and/or
14+
/// Use `foreach` if the operation requires no copies and high performance. Use the iterator variant if you need to collect the results and/or
1415
/// want to have access to the Rust iterator API.
1516
///
1617
/// ## Example usage
1718
///
1819
/// Here we show how to extract values to communicate them back to the Redis client. We assume that the following hash key is setup:
1920
///
20-
/// ```no_run
21+
/// ```text
2122
/// HSET user:123 name Alice age 29 location Austin
2223
/// ```
2324
///
2425
/// For using the `foreach` method:
2526
///
26-
/// ```no_run
27+
/// ```ignore
2728
/// fn example_scan_key_foreach(ctx: &Context) -> RedisResult {
2829
/// let key = ctx.open_key_with_flags("user:123", KeyFlags::NOEFFECTS | KeyFlags::NOEXPIRE | KeyFlags::ACCESS_EXPIRED );
2930
/// let cursor = ScanKeyCursor::new(key);
@@ -41,7 +42,7 @@ use crate::{key::RedisKey, raw, Context, RedisString};
4142
///
4243
/// For using the `iter` method:
4344
///
44-
/// ```no_run
45+
/// ```ignore
4546
/// fn example_scan_key_foreach(ctx: &Context) -> RedisResult {
4647
/// let mut res = Vec::new();
4748
/// let key = ctx.open_key_with_flags("user:123", KeyFlags::NOEFFECTS | KeyFlags::NOEXPIRE | KeyFlags::ACCESS_EXPIRED );
@@ -70,16 +71,18 @@ pub struct ScanKeyCursor {
7071
}
7172

7273
impl ScanKeyCursor {
74+
/// Creates a new scan cursor for the given key.
7375
pub fn new(key: RedisKey) -> Self {
7476
let inner_cursor = unsafe { raw::RedisModule_ScanCursorCreate.unwrap()() };
7577
Self { key, inner_cursor }
7678
}
7779

80+
/// Restarts the cursor from the beginning.
7881
pub fn restart(&self) {
7982
unsafe { raw::RedisModule_ScanCursorRestart.unwrap()(self.inner_cursor) };
8083
}
8184

82-
/// Implements a callback based foreach loop over all fields and values in the hash key, use for optimal performance.
85+
/// Implements a callback based foreach loop over all fields and values in the hash key, use that for optimal performance.
8386
pub fn foreach<F: Fn(&RedisKey, &RedisString, &RedisString)>(&self, f: F) {
8487
// Safety: Assumption: c-side initialized the function ptr and it is is never changed,
8588
// i.e. after module initialization the function pointers stay valid till the end of the program.
@@ -98,9 +101,12 @@ impl ScanKeyCursor {
98101
}
99102
}
100103

104+
/// Returns an iterator over all fields and values in the hash key.
105+
///
106+
/// As the rust loop scope is detached from the C callback
107+
/// we need to buffer the field/value pairs. That has performance implications. They are lower if the field/value pairs are
108+
/// copied anyway, but even in that case not as fast as using the [`ScanKeyCursor::foreach`] method.
101109
pub fn iter(&self) -> ScanKeyCursorIterator<'_> {
102-
let ctx = Context::new(self.key.ctx);
103-
ctx.log_notice("Starting ScanKeyCursor iteration");
104110
ScanKeyCursorIterator {
105111
cursor: self,
106112
buf: Vec::new(),
@@ -117,6 +123,7 @@ impl Drop for ScanKeyCursor {
117123

118124
pub type ScanKeyIteratorItem = (RedisString, RedisString);
119125

126+
/// An iterator (state) over the field/value pairs of a hash key.
120127
pub struct ScanKeyCursorIterator<'a> {
121128
/// The cursor that is used for the iteration
122129
cursor: &'a ScanKeyCursor,
@@ -199,9 +206,10 @@ impl ScanKeyCursorIterator<'_> {
199206
}
200207
}
201208

202-
/// The callback that is called by `RedisModule_ScanKey` to return the field and value strings.
209+
/// The callback that is used by [`ScanKeyCursor::foreach`] as argument to `RedisModule_ScanKey`.
203210
///
204-
/// The `data` pointer is a stack slot of type `RawData` that is used to pass the data back to the iterator.
211+
/// The `data` pointer is the closure given to [`ScanKeyCursor::foreach`] and the callback forwards
212+
/// references to the key, field and value to that closure.
205213
unsafe extern "C" fn foreach_callback<F: Fn(&RedisKey, &RedisString, &RedisString)>(
206214
key: *mut raw::RedisModuleKey,
207215
field: *mut raw::RedisModuleString,
@@ -224,43 +232,37 @@ unsafe extern "C" fn foreach_callback<F: Fn(&RedisKey, &RedisString, &RedisStrin
224232
key.take(); // we're not the owner of the key either
225233
}
226234

227-
/// The callback that is called by `RedisModule_ScanKey` to return the field and value strings.
228-
///
229-
/// The `data` pointer is a stack slot of type `RawData` that is used to pass the data back to the iterator.
235+
/// The callback that is used inside the iterator variant accessible via [`ScanKeyCursor::iter`] for `RedisModule_ScanKey`.
236+
///
237+
/// It buffers copies of the field and value strings as the lifetime of them ends when with the call
238+
/// to `RedisModule_ScanKey` going out of scope.
239+
///
240+
/// The `data` pointer is a stack slot of type [StackSlot] that is used to pass the data back to the iterator.
230241
unsafe extern "C" fn iterator_callback(
231242
_key: *mut raw::RedisModuleKey,
232243
field: *mut raw::RedisModuleString,
233244
value: *mut raw::RedisModuleString,
234245
data: *mut c_void,
235246
) {
236-
// SAFETY: this is the responsibility of the caller, see only usage below in `next()`
237-
// `data` is a stack slot of type Data
247+
// `data` is a stack slot
238248
let slot = data.cast::<StackSlot>();
239249
let slot = &mut (*slot);
240250

241-
// todo: use new-type with refcount handling for better performance
251+
// todo: use new-type with refcount handling for better performance, otherwise we have to copy at this point
252+
// we know that this callback will be called in a loop scope and that we
253+
// need to store the RedisString longer than the ScanKey invocation but not much
254+
// longer and in case of batched results we don't need to store everything in memory
255+
// but only the last batch.
242256
let field = raw::RedisModule_CreateStringFromString.unwrap()(slot.ctx.get_raw(), field);
243257
let value = raw::RedisModule_CreateStringFromString.unwrap()(slot.ctx.get_raw(), value);
244258

245259
let field = RedisString::from_redis_module_string(slot.ctx.get_raw(), field);
246260
let value = RedisString::from_redis_module_string(slot.ctx.get_raw(), value);
247261

248-
if slot.buf.is_empty() {
249-
let out = format!("CB - Value tu return - Field: {}, Value: {}", field, value);
250-
slot.ctx.log_notice(&out);
251-
} else {
252-
// This is the case where the callback is called multiple times.
253-
// We need to buffer the data in the iterator state.
254-
let out = format!(
255-
"CB - Buffer for future use - Field: {}, Value: {}",
256-
field, value
257-
);
258-
slot.ctx.log_notice(&out);
259-
}
260262
slot.buf.push((field, value));
261263
}
262264

263-
// Implements an iterator for `KeysCursor` that yields (RedisKey, *mut RedisModuleString, *mut RedisModuleString) in a Rust for loop.
265+
// Implements an iterator for `KeysCursor` that yields (RedisString, RedisString) in a Rust for loop.
264266
// This is a wrapper around the RedisModule_ScanKey function from the C API and uses a pattern to get the values from the callback that
265267
// is also used in stack unwinding scenarios. There is not common term for that but here we can think of it as a "stack slot" pattern.
266268
impl Iterator for ScanKeyCursorIterator<'_> {

src/key.rs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,7 @@ impl RedisKey {
110110
/// Will panic if `RedisModule_KeyType` is missing in redismodule.h
111111
#[must_use]
112112
pub fn key_type(&self) -> raw::KeyType {
113-
let discriminant = unsafe { raw::RedisModule_KeyType.unwrap()(self.key_inner) };
114-
KeyType::from_repr(discriminant as u32).unwrap()
113+
unsafe { raw::RedisModule_KeyType.unwrap()(self.key_inner) }.into()
115114
}
116115

117116
/// Detects whether the key pointer given to us by Redis is null.
@@ -380,8 +379,7 @@ impl RedisKeyWritable {
380379
/// Will panic if `RedisModule_KeyType` is missing in redismodule.h
381380
#[must_use]
382381
pub fn key_type(&self) -> raw::KeyType {
383-
let discriminant = unsafe { raw::RedisModule_KeyType.unwrap()(self.key_inner) };
384-
KeyType::from_repr(discriminant as u32).unwrap()
382+
unsafe { raw::RedisModule_KeyType.unwrap()(self.key_inner) }.into()
385383
}
386384

387385
pub fn open_with_redis_string(
@@ -672,7 +670,7 @@ fn to_raw_mode(mode: KeyMode) -> raw::KeyMode {
672670
/// Will panic if `RedisModule_KeyType` or `RedisModule_ModuleTypeGetType` are missing in redismodule.h
673671
#[allow(clippy::not_unsafe_ptr_arg_deref)]
674672
pub fn verify_type(key_inner: *mut raw::RedisModuleKey, redis_type: &RedisType) -> RedisResult {
675-
let key_type: KeyType = KeyType::from_repr(unsafe { raw::RedisModule_KeyType.unwrap()(key_inner) as u32 }).unwrap();
673+
let key_type: KeyType = unsafe { raw::RedisModule_KeyType.unwrap()(key_inner) }.into();
676674

677675
if key_type != KeyType::Empty {
678676
// The key exists; check its type

src/raw.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,6 @@ bitflags! {
2929
pub struct KeyMode: c_int {
3030
const READ = REDISMODULE_READ as c_int;
3131
const WRITE = REDISMODULE_WRITE as c_int;
32-
33-
const KEYOPEN_NOTOUCH = REDISMODULE_OPEN_KEY_NOTOUCH as c_int;
34-
const KEYOPEN_NONOTIFY = REDISMODULE_OPEN_KEY_NONOTIFY as c_int;
35-
const KEYOPEN_NOSTATS = REDISMODULE_OPEN_KEY_NOSTATS as c_int;
36-
const KEYOPEN_NOEFFECTS = REDISMODULE_OPEN_KEY_NOEFFECTS as c_int;
37-
const KEYOPEN_NOEXPIRE = REDISMODULE_OPEN_KEY_NOEXPIRE as c_int;
38-
const KEYOPEN_ACCESSEXPIRED = REDISMODULE_OPEN_KEY_ACCESS_EXPIRED as c_int;
3932
}
4033
}
4134

@@ -60,7 +53,14 @@ pub enum KeyType {
6053
Stream = REDISMODULE_KEYTYPE_STREAM,
6154
}
6255

63-
#[derive(Primitive, Debug, PartialEq, Eq)]
56+
impl From<c_int> for KeyType {
57+
fn from(v: c_int) -> Self {
58+
Self::from_i32(v).unwrap()
59+
}
60+
}
61+
62+
#[repr(u32)]
63+
#[derive(Primitive, Debug, PartialEq, Eq, strum::FromRepr)]
6464
pub enum Where {
6565
ListHead = REDISMODULE_LIST_HEAD,
6666
ListTail = REDISMODULE_LIST_TAIL,

src/redismodule.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -273,10 +273,6 @@ impl RedisString {
273273
impl Drop for RedisString {
274274
fn drop(&mut self) {
275275
if !self.inner.is_null() {
276-
let ctx = Context::new(self.ctx);
277-
let out = format!("Dropping RedisString '{}'", self.to_string_lossy());
278-
ctx.log_notice(&out);
279-
280276
unsafe {
281277
raw::RedisModule_FreeString.unwrap()(self.ctx, self.inner);
282278
}

tests/integration.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,41 @@ fn test_scan() -> Result<()> {
200200
Ok(())
201201
}
202202

203+
#[test]
204+
fn test_scan_key_it() -> Result<()> {
205+
let mut con = TestConnection::new("scan_keys");
206+
207+
redis::cmd("hset")
208+
.arg(&[
209+
"user:123", "name", "Alice", "age", "29", "location", "Austin",
210+
])
211+
.query::<()>(&mut con)
212+
.with_context(|| "failed to hset")?;
213+
214+
let res: Vec<String> = redis::cmd("scan_key_it")
215+
.arg(&["user:123"])
216+
.query(&mut con)?;
217+
assert_eq!(&res, &["name", "Alice", "age", "29", "location", "Austin"]);
218+
Ok(())
219+
}
220+
221+
#[test]
222+
fn test_scan_key_foreach() -> Result<()> {
223+
let mut con = TestConnection::new("scan_keys");
224+
redis::cmd("hset")
225+
.arg(&[
226+
"user:123", "name", "Alice", "age", "29", "location", "Austin",
227+
])
228+
.query::<()>(&mut con)
229+
.with_context(|| "failed to hset")?;
230+
231+
let res: Vec<String> = redis::cmd("scan_key_fe")
232+
.arg(&["user:123"])
233+
.query(&mut con)?;
234+
assert_eq!(&res, &["name", "Alice", "age", "29", "location", "Austin"]);
235+
Ok(())
236+
}
237+
203238
#[test]
204239
fn test_stream_reader() -> Result<()> {
205240
let mut con = TestConnection::new("stream");

0 commit comments

Comments
 (0)