Skip to content

Commit bc7df53

Browse files
authored
Add support for RedisModule_CreateTimer (#67)
* Add support for Redis timers - Wrappers for RedisModule_CreateTimer, StopTimer and GetTimerInfo. - Type-safe handling of user-supplied data and callback. - Ownership of data is handled using Rust's ownership semantics.
1 parent 84b3f20 commit bc7df53

File tree

4 files changed

+195
-1
lines changed

4 files changed

+195
-1
lines changed

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ crate-type = ["cdylib"]
1717
name = "keys_pos"
1818
crate-type = ["cdylib"]
1919

20+
[[example]]
21+
name = "timer"
22+
crate-type = ["cdylib"]
23+
required-features = ["experimental-api"]
24+
2025
[[example]]
2126
name = "data_type"
2227
crate-type = ["cdylib"]

examples/timer.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#[macro_use]
2+
extern crate redis_module;
3+
4+
use redis_module::{Context, NextArg, RedisError, RedisResult};
5+
use std::time::Duration;
6+
7+
fn callback(ctx: &Context, data: String) {
8+
ctx.log_debug(format!("[callback]: {}", data).as_str());
9+
}
10+
11+
type MyData = String;
12+
13+
fn timer_create(ctx: &Context, args: Vec<String>) -> RedisResult {
14+
let mut args = args.into_iter().skip(1);
15+
let duration = args.next_i64()?;
16+
let data: MyData = args.next_string()?;
17+
18+
let timer_id = ctx.create_timer(Duration::from_millis(duration as u64), callback, data);
19+
20+
return Ok(format!("{}", timer_id).into());
21+
}
22+
23+
fn timer_info(ctx: &Context, args: Vec<String>) -> RedisResult {
24+
let mut args = args.into_iter().skip(1);
25+
let timer_id = args.next_u64()?;
26+
27+
let (remaining, data): (_, &MyData) = ctx.get_timer_info(timer_id)?;
28+
let reply = format!("Remaining: {:?}, data: {:?}", remaining, data);
29+
30+
return Ok(reply.into());
31+
}
32+
33+
fn timer_stop(ctx: &Context, args: Vec<String>) -> RedisResult {
34+
let mut args = args.into_iter().skip(1);
35+
let timer_id = args.next_u64()?;
36+
37+
let data: MyData = ctx.stop_timer(timer_id)?;
38+
let reply = format!("Data: {:?}", data);
39+
40+
return Ok(reply.into());
41+
}
42+
43+
//////////////////////////////////////////////////////
44+
45+
redis_module! {
46+
name: "timer",
47+
version: 1,
48+
data_types: [],
49+
commands: [
50+
["timer.create", timer_create, "", 0, 0, 0],
51+
["timer.info", timer_info, "", 0, 0, 0],
52+
["timer.stop", timer_stop, "", 0, 0, 0],
53+
],
54+
}

src/context.rs renamed to src/context/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@ use crate::raw;
77
use crate::LogLevel;
88
use crate::{RedisError, RedisResult, RedisString, RedisValue};
99

10+
#[cfg(feature = "experimental-api")]
11+
mod timer;
12+
1013
/// `Context` is a structure that's designed to give us a high-level interface to
1114
/// the Redis module API by abstracting away the raw C FFI calls.
1215
pub struct Context {
13-
ctx: *mut raw::RedisModuleCtx,
16+
pub(crate) ctx: *mut raw::RedisModuleCtx,
1417
}
1518

1619
impl Context {

src/context/timer.rs

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
use std::convert::TryInto;
2+
use std::ffi::c_void;
3+
use std::time::Duration;
4+
5+
use crate::raw;
6+
use crate::raw::RedisModuleTimerID;
7+
use crate::{Context, RedisError};
8+
9+
// We use `repr(C)` since we access the underlying data field directly.
10+
// The order matters: the data field must come first.
11+
#[repr(C)]
12+
struct CallbackData<F: FnOnce(&Context, T), T> {
13+
data: T,
14+
callback: F,
15+
}
16+
17+
impl Context {
18+
/// Wrapper for `RedisModule_CreateTimer`.
19+
///
20+
/// This function takes ownership of the provided data, and transfers it to Redis.
21+
/// The callback will get the original data back in a type safe manner.
22+
/// When the callback is done, the data will be dropped.
23+
pub fn create_timer<F, T>(&self, period: Duration, callback: F, data: T) -> RedisModuleTimerID
24+
where
25+
F: FnOnce(&Context, T),
26+
{
27+
let cb_data = CallbackData { data, callback };
28+
29+
// Store the user-provided data on the heap before passing ownership of it to Redis,
30+
// so that it will outlive the current scope.
31+
let data = Box::from(cb_data);
32+
33+
// Take ownership of the data inside the box and obtain a raw pointer to pass to Redis.
34+
let data = Box::into_raw(data);
35+
36+
let timer_id = unsafe {
37+
raw::RedisModule_CreateTimer.unwrap()(
38+
self.ctx,
39+
period
40+
.as_millis()
41+
.try_into()
42+
.expect("Value must fit in 64 bits"),
43+
Some(raw_callback::<F, T>),
44+
data as *mut c_void,
45+
)
46+
};
47+
48+
timer_id
49+
}
50+
51+
/// Wrapper for `RedisModule_StopTimer`.
52+
///
53+
/// The caller is responsible for specifying the correct type for the returned data.
54+
/// This function has no way to know what the original type of the data was, so the
55+
/// same data type that was used for `create_timer` needs to be passed here to ensure
56+
/// their types are identical.
57+
pub fn stop_timer<T>(&self, timer_id: RedisModuleTimerID) -> Result<T, RedisError> {
58+
let mut data: *mut c_void = std::ptr::null_mut();
59+
60+
let status: raw::Status =
61+
unsafe { raw::RedisModule_StopTimer.unwrap()(self.ctx, timer_id, &mut data) }.into();
62+
63+
if status != raw::Status::Ok {
64+
return Err(RedisError::Str(
65+
"RedisModule_StopTimer failed, timer may not exist",
66+
));
67+
}
68+
69+
let data: T = take_data(data);
70+
return Ok(data);
71+
}
72+
73+
/// Wrapper for `RedisModule_GetTimerInfo`.
74+
///
75+
/// The caller is responsible for specifying the correct type for the returned data.
76+
/// This function has no way to know what the original type of the data was, so the
77+
/// same data type that was used for `create_timer` needs to be passed here to ensure
78+
/// their types are identical.
79+
pub fn get_timer_info<T>(
80+
&self,
81+
timer_id: RedisModuleTimerID,
82+
) -> Result<(Duration, &T), RedisError> {
83+
let mut remaining: u64 = 0;
84+
let mut data: *mut c_void = std::ptr::null_mut();
85+
86+
let status: raw::Status = unsafe {
87+
raw::RedisModule_GetTimerInfo.unwrap()(self.ctx, timer_id, &mut remaining, &mut data)
88+
}
89+
.into();
90+
91+
if status != raw::Status::Ok {
92+
return Err(RedisError::Str(
93+
"RedisModule_GetTimerInfo failed, timer may not exist",
94+
));
95+
}
96+
97+
// Cast the *mut c_void supplied by the Redis API to a raw pointer of our custom type.
98+
let data = data as *mut T;
99+
100+
// Dereference the raw pointer (we know this is safe, since Redis should return our
101+
// original pointer which we know to be good) and turn it into a safe reference
102+
let data = unsafe { &*data };
103+
104+
Ok((Duration::from_millis(remaining), data))
105+
}
106+
}
107+
108+
fn take_data<T>(data: *mut c_void) -> T {
109+
// Cast the *mut c_void supplied by the Redis API to a raw pointer of our custom type.
110+
let data = data as *mut T;
111+
112+
// Take back ownership of the original boxed data, so we can unbox it safely.
113+
// If we don't do this, the data's memory will be leaked.
114+
let data = unsafe { Box::from_raw(data) };
115+
116+
*data
117+
}
118+
119+
extern "C" fn raw_callback<F, T>(ctx: *mut raw::RedisModuleCtx, data: *mut c_void)
120+
where
121+
F: FnOnce(&Context, T),
122+
{
123+
let ctx = &Context::new(ctx);
124+
125+
if data.is_null() {
126+
ctx.log_debug("[callback] Data is null; this should not happen!");
127+
return;
128+
}
129+
130+
let cb_data: CallbackData<F, T> = take_data(data);
131+
(cb_data.callback)(ctx, cb_data.data);
132+
}

0 commit comments

Comments
 (0)