Skip to main content

redis_module/
configuration.rs

1use crate::context::thread_safe::{RedisGILGuard, RedisLockIndicator};
2use crate::{raw, CallOptionResp, CallOptionsBuilder, CallResult, RedisValue};
3use crate::{Context, RedisError, RedisString};
4use bitflags::bitflags;
5use std::ffi::{CStr, CString};
6use std::marker::PhantomData;
7use std::os::raw::{c_char, c_int, c_longlong, c_void};
8use std::sync::atomic::{AtomicBool, AtomicI64, Ordering};
9use std::sync::Mutex;
10
11bitflags! {
12    /// Configuration options
13    pub struct ConfigurationFlags : u32 {
14        /// The default flags for a config. This creates a config that can be modified after startup.
15        const DEFAULT = raw::REDISMODULE_CONFIG_DEFAULT;
16
17        /// This config can only be provided loading time.
18        const IMMUTABLE = raw::REDISMODULE_CONFIG_IMMUTABLE;
19
20        /// The value stored in this config is redacted from all logging.
21        const SENSITIVE = raw::REDISMODULE_CONFIG_SENSITIVE;
22
23        /// The name is hidden from `CONFIG GET` with pattern matching.
24        const HIDDEN = raw::REDISMODULE_CONFIG_HIDDEN;
25
26        /// This config will be only be modifiable based off the value of enable-protected-configs.
27        const PROTECTED = raw::REDISMODULE_CONFIG_PROTECTED;
28
29        /// This config is not modifiable while the server is loading data.
30        const DENY_LOADING = raw::REDISMODULE_CONFIG_DENY_LOADING;
31
32        /// For numeric configs, this config will convert data unit notations into their byte equivalent.
33        const MEMORY = raw::REDISMODULE_CONFIG_MEMORY;
34
35        /// For enum configs, this config will allow multiple entries to be combined as bit flags.
36        const BITFLAGS = raw::REDISMODULE_CONFIG_BITFLAGS;
37
38        /// For configs, will avoid prefixing the config name with the module name.
39        const UNPREFIXED = raw::REDISMODULE_CONFIG_UNPREFIXED;
40    }
41}
42
43#[macro_export]
44macro_rules! enum_configuration {
45    ($(#[$meta:meta])* $vis:vis enum $name:ident {
46        $($(#[$vmeta:meta])* $vname:ident = $val:expr,)*
47    }) => {
48        use $crate::configuration::EnumConfigurationValue;
49        $(#[$meta])*
50        $vis enum $name {
51            $($(#[$vmeta])* $vname = $val,)*
52        }
53
54        impl std::convert::TryFrom<i32> for $name {
55            type Error = $crate::RedisError;
56
57            fn try_from(v: i32) -> Result<Self, Self::Error> {
58                match v {
59                    $(x if x == $name::$vname as i32 => Ok($name::$vname),)*
60                    _ => Err($crate::RedisError::Str("Value is not supported")),
61                }
62            }
63        }
64
65        impl std::convert::From<$name> for i32 {
66            fn from(val: $name) -> Self {
67                val as i32
68            }
69        }
70
71        impl EnumConfigurationValue for $name {
72            fn get_options(&self) -> (Vec<String>, Vec<i32>) {
73                (vec![$(stringify!($vname).to_string(),)*], vec![$($val,)*])
74            }
75        }
76
77        impl Clone for $name {
78            fn clone(&self) -> Self {
79                match self {
80                    $($name::$vname => $name::$vname,)*
81                }
82            }
83        }
84    }
85}
86
87/// [`ConfigurationContext`] is used as a special context that indicate that we are
88/// running with the Redis GIL is held but we should not perform all the regular
89/// operation we can perfrom on the regular Context.
90pub struct ConfigurationContext {
91    _dummy: usize, // We set some none public vairable here so user will not be able to construct such object
92}
93
94impl ConfigurationContext {
95    fn new() -> ConfigurationContext {
96        ConfigurationContext { _dummy: 0 }
97    }
98}
99
100unsafe impl RedisLockIndicator for ConfigurationContext {}
101
102pub trait ConfigurationValue<T>: Sync + Send {
103    fn get(&self, ctx: &ConfigurationContext) -> T;
104    fn set(&self, ctx: &ConfigurationContext, val: T) -> Result<(), RedisError>;
105}
106
107pub trait EnumConfigurationValue: TryFrom<i32, Error = RedisError> + Into<i32> + Clone {
108    fn get_options(&self) -> (Vec<String>, Vec<i32>);
109}
110
111impl<T: Clone> ConfigurationValue<T> for RedisGILGuard<T> {
112    fn get(&self, ctx: &ConfigurationContext) -> T {
113        let value = self.lock(ctx);
114        value.clone()
115    }
116    fn set(&self, ctx: &ConfigurationContext, val: T) -> Result<(), RedisError> {
117        let mut value = self.lock(ctx);
118        *value = val;
119        Ok(())
120    }
121}
122
123impl<T: Clone + Send> ConfigurationValue<T> for Mutex<T> {
124    fn get(&self, _ctx: &ConfigurationContext) -> T {
125        let value = self.lock().unwrap();
126        value.clone()
127    }
128    fn set(&self, _ctx: &ConfigurationContext, val: T) -> Result<(), RedisError> {
129        let mut value = self.lock().unwrap();
130        *value = val;
131        Ok(())
132    }
133}
134
135impl ConfigurationValue<i64> for AtomicI64 {
136    fn get(&self, _ctx: &ConfigurationContext) -> i64 {
137        self.load(Ordering::Relaxed)
138    }
139    fn set(&self, _ctx: &ConfigurationContext, val: i64) -> Result<(), RedisError> {
140        self.store(val, Ordering::Relaxed);
141        Ok(())
142    }
143}
144
145impl ConfigurationValue<RedisString> for RedisGILGuard<String> {
146    fn get(&self, ctx: &ConfigurationContext) -> RedisString {
147        let value = self.lock(ctx);
148        RedisString::create(None, value.as_str())
149    }
150    fn set(&self, ctx: &ConfigurationContext, val: RedisString) -> Result<(), RedisError> {
151        let mut value = self.lock(ctx);
152        *value = val.try_as_str()?.to_string();
153        Ok(())
154    }
155}
156
157impl ConfigurationValue<RedisString> for Mutex<String> {
158    fn get(&self, _ctx: &ConfigurationContext) -> RedisString {
159        let value = self.lock().unwrap();
160        RedisString::create(None, value.as_str())
161    }
162    fn set(&self, _ctx: &ConfigurationContext, val: RedisString) -> Result<(), RedisError> {
163        let mut value = self.lock().unwrap();
164        *value = val.try_as_str()?.to_string();
165        Ok(())
166    }
167}
168
169impl ConfigurationValue<bool> for AtomicBool {
170    fn get(&self, _ctx: &ConfigurationContext) -> bool {
171        self.load(Ordering::Relaxed)
172    }
173    fn set(&self, _ctx: &ConfigurationContext, val: bool) -> Result<(), RedisError> {
174        self.store(val, Ordering::Relaxed);
175        Ok(())
176    }
177}
178
179type OnUpdatedCallback<T> = Box<dyn Fn(&ConfigurationContext, &str, &'static T)>;
180
181struct ConfigrationPrivateData<G, T: ConfigurationValue<G> + 'static> {
182    variable: &'static T,
183    on_changed: Option<OnUpdatedCallback<T>>,
184    phantom: PhantomData<G>,
185}
186
187impl<G, T: ConfigurationValue<G> + 'static> ConfigrationPrivateData<G, T> {
188    fn set_val(&self, name: *const c_char, val: G, err: *mut *mut raw::RedisModuleString) -> c_int {
189        // we know the GIL is held so it is safe to use Context::dummy().
190        let configuration_ctx = ConfigurationContext::new();
191        if let Err(e) = self.variable.set(&configuration_ctx, val) {
192            let error_msg = RedisString::create(None, e.to_string().as_str());
193            unsafe { *err = error_msg.take() };
194            return raw::REDISMODULE_ERR as i32;
195        }
196        let c_str_name = unsafe { CStr::from_ptr(name) };
197        if let Some(v) = self.on_changed.as_ref() {
198            v(
199                &configuration_ctx,
200                c_str_name.to_str().unwrap(),
201                self.variable,
202            )
203        }
204        raw::REDISMODULE_OK as i32
205    }
206
207    fn get_val(&self) -> G {
208        self.variable.get(&ConfigurationContext::new())
209    }
210}
211
212extern "C" fn i64_configuration_set<T: ConfigurationValue<i64> + 'static>(
213    name: *const c_char,
214    val: c_longlong,
215    privdata: *mut c_void,
216    err: *mut *mut raw::RedisModuleString,
217) -> c_int {
218    let private_data = unsafe { &*(privdata as *const ConfigrationPrivateData<i64, T>) };
219    private_data.set_val(name, val, err)
220}
221
222extern "C" fn i64_configuration_get<T: ConfigurationValue<i64> + 'static>(
223    _name: *const c_char,
224    privdata: *mut c_void,
225) -> c_longlong {
226    let private_data = unsafe { &*(privdata as *const ConfigrationPrivateData<i64, T>) };
227    private_data.get_val()
228}
229
230#[expect(clippy::too_many_arguments)]
231pub fn register_i64_configuration<T: ConfigurationValue<i64>>(
232    ctx: &Context,
233    name: &str,
234    variable: &'static T,
235    default: i64,
236    min: i64,
237    max: i64,
238    flags: ConfigurationFlags,
239    on_changed: Option<OnUpdatedCallback<T>>,
240) {
241    let name = CString::new(name).unwrap();
242    let config_private_data = ConfigrationPrivateData {
243        variable,
244        on_changed,
245        phantom: PhantomData::<i64>,
246    };
247    unsafe {
248        raw::RedisModule_RegisterNumericConfig.unwrap()(
249            ctx.ctx,
250            name.as_ptr(),
251            default,
252            flags.bits(),
253            min,
254            max,
255            Some(i64_configuration_get::<T>),
256            Some(i64_configuration_set::<T>),
257            None,
258            Box::into_raw(Box::new(config_private_data)) as *mut c_void,
259        );
260    }
261}
262
263fn find_config_value<'a>(args: &'a [RedisString], name: &str) -> Option<&'a RedisString> {
264    args.iter()
265        .skip_while(|item| !item.as_slice().eq(name.as_bytes()))
266        .nth(1)
267}
268
269pub fn get_i64_default_config_value(
270    args: &[RedisString],
271    name: &str,
272    default: i64,
273) -> Result<i64, RedisError> {
274    find_config_value(args, name).map_or(Ok(default), |arg| {
275        arg.try_as_str()?
276            .parse::<i64>()
277            .map_err(|e| RedisError::String(e.to_string()))
278    })
279}
280
281extern "C" fn string_configuration_set<T: ConfigurationValue<RedisString> + 'static>(
282    name: *const c_char,
283    val: *mut raw::RedisModuleString,
284    privdata: *mut c_void,
285    err: *mut *mut raw::RedisModuleString,
286) -> c_int {
287    let new_val = RedisString::new(None, val);
288    let private_data = unsafe { &*(privdata as *const ConfigrationPrivateData<RedisString, T>) };
289    private_data.set_val(name, new_val, err)
290}
291
292extern "C" fn string_configuration_get<T: ConfigurationValue<RedisString> + 'static>(
293    _name: *const c_char,
294    privdata: *mut c_void,
295) -> *mut raw::RedisModuleString {
296    let private_data = unsafe { &*(privdata as *const ConfigrationPrivateData<RedisString, T>) };
297    // we know the GIL is held so it is safe to use Context::dummy().
298    private_data
299        .variable
300        .get(&ConfigurationContext::new())
301        .take()
302}
303
304pub fn register_string_configuration<T: ConfigurationValue<RedisString>>(
305    ctx: &Context,
306    name: &str,
307    variable: &'static T,
308    default: &str,
309    flags: ConfigurationFlags,
310    on_changed: Option<OnUpdatedCallback<T>>,
311) {
312    let name = CString::new(name).unwrap();
313    let default = CString::new(default).unwrap();
314    let config_private_data = ConfigrationPrivateData {
315        variable,
316        on_changed,
317        phantom: PhantomData::<RedisString>,
318    };
319    unsafe {
320        raw::RedisModule_RegisterStringConfig.unwrap()(
321            ctx.ctx,
322            name.as_ptr(),
323            default.as_ptr(),
324            flags.bits(),
325            Some(string_configuration_get::<T>),
326            Some(string_configuration_set::<T>),
327            None,
328            Box::into_raw(Box::new(config_private_data)) as *mut c_void,
329        );
330    }
331}
332
333pub fn get_string_default_config_value<'a>(
334    args: &'a [RedisString],
335    name: &str,
336    default: &'a str,
337) -> Result<&'a str, RedisError> {
338    find_config_value(args, name).map_or(Ok(default), |arg| arg.try_as_str())
339}
340
341extern "C" fn bool_configuration_set<T: ConfigurationValue<bool> + 'static>(
342    name: *const c_char,
343    val: i32,
344    privdata: *mut c_void,
345    err: *mut *mut raw::RedisModuleString,
346) -> c_int {
347    let private_data = unsafe { &*(privdata as *const ConfigrationPrivateData<bool, T>) };
348    private_data.set_val(name, val != 0, err)
349}
350
351extern "C" fn bool_configuration_get<T: ConfigurationValue<bool> + 'static>(
352    _name: *const c_char,
353    privdata: *mut c_void,
354) -> c_int {
355    let private_data = unsafe { &*(privdata as *const ConfigrationPrivateData<bool, T>) };
356    private_data.get_val() as i32
357}
358
359pub fn register_bool_configuration<T: ConfigurationValue<bool>>(
360    ctx: &Context,
361    name: &str,
362    variable: &'static T,
363    default: bool,
364    flags: ConfigurationFlags,
365    on_changed: Option<OnUpdatedCallback<T>>,
366) {
367    let name = CString::new(name).unwrap();
368    let config_private_data = ConfigrationPrivateData {
369        variable,
370        on_changed,
371        phantom: PhantomData::<bool>,
372    };
373    unsafe {
374        raw::RedisModule_RegisterBoolConfig.unwrap()(
375            ctx.ctx,
376            name.as_ptr(),
377            default as i32,
378            flags.bits(),
379            Some(bool_configuration_get::<T>),
380            Some(bool_configuration_set::<T>),
381            None,
382            Box::into_raw(Box::new(config_private_data)) as *mut c_void,
383        );
384    }
385}
386
387pub fn get_bool_default_config_value(
388    args: &[RedisString],
389    name: &str,
390    default: bool,
391) -> Result<bool, RedisError> {
392    find_config_value(args, name).map_or(Ok(default), |arg| Ok(arg.try_as_str()? == "yes"))
393}
394
395extern "C" fn enum_configuration_set<
396    G: EnumConfigurationValue,
397    T: ConfigurationValue<G> + 'static,
398>(
399    name: *const c_char,
400    val: i32,
401    privdata: *mut c_void,
402    err: *mut *mut raw::RedisModuleString,
403) -> c_int {
404    let private_data = unsafe { &*(privdata as *const ConfigrationPrivateData<G, T>) };
405    let val: Result<G, _> = val.try_into();
406    match val {
407        Ok(val) => private_data.set_val(name, val, err),
408        Err(e) => {
409            let error_msg = RedisString::create(None, e.to_string().as_str());
410            unsafe { *err = error_msg.take() };
411            raw::REDISMODULE_ERR as i32
412        }
413    }
414}
415
416extern "C" fn enum_configuration_get<
417    G: EnumConfigurationValue,
418    T: ConfigurationValue<G> + 'static,
419>(
420    _name: *const c_char,
421    privdata: *mut c_void,
422) -> c_int {
423    let private_data = unsafe { &*(privdata as *const ConfigrationPrivateData<G, T>) };
424    private_data.get_val().into()
425}
426
427pub fn register_enum_configuration<G: EnumConfigurationValue, T: ConfigurationValue<G>>(
428    ctx: &Context,
429    name: &str,
430    variable: &'static T,
431    default: G,
432    flags: ConfigurationFlags,
433    on_changed: Option<OnUpdatedCallback<T>>,
434) {
435    let name = CString::new(name).unwrap();
436    let (names, vals) = default.get_options();
437    assert_eq!(names.len(), vals.len());
438    let names: Vec<CString> = names
439        .into_iter()
440        .map(|v| CString::new(v).unwrap())
441        .collect();
442    let config_private_data = ConfigrationPrivateData {
443        variable,
444        on_changed,
445        phantom: PhantomData::<G>,
446    };
447    unsafe {
448        raw::RedisModule_RegisterEnumConfig.unwrap()(
449            ctx.ctx,
450            name.as_ptr(),
451            default.into(),
452            flags.bits(),
453            names
454                .iter()
455                .map(|v| v.as_ptr())
456                .collect::<Vec<*const c_char>>()
457                .as_mut_ptr(),
458            vals.as_ptr(),
459            names.len() as i32,
460            Some(enum_configuration_get::<G, T>),
461            Some(enum_configuration_set::<G, T>),
462            None,
463            Box::into_raw(Box::new(config_private_data)) as *mut c_void,
464        );
465    }
466}
467
468pub fn get_enum_default_config_value<G: EnumConfigurationValue>(
469    args: &[RedisString],
470    name: &str,
471    default: G,
472) -> Result<G, RedisError> {
473    find_config_value(args, name).map_or(Ok(default.clone()), |arg| {
474        let (names, vals) = default.get_options();
475        let (index, _name) = names
476            .into_iter()
477            .enumerate()
478            .find(|(_index, item)| item.as_bytes().eq(arg.as_slice()))
479            .ok_or(RedisError::String(format!(
480                "Enum '{}' not exists",
481                arg.to_string_lossy()
482            )))?;
483        G::try_from(vals[index])
484    })
485}
486
487pub fn module_config_get(
488    ctx: &Context,
489    args: Vec<RedisString>,
490    name: &str,
491) -> Result<RedisValue, RedisError> {
492    let mut args: Vec<String> = args
493        .into_iter()
494        .skip(1)
495        .map(|e| format!("{}.{}", name, e.to_string_lossy()))
496        .collect();
497    args.insert(0, "get".into());
498    let res: CallResult = ctx.call_ext(
499        "config",
500        &CallOptionsBuilder::new()
501            .errors_as_replies()
502            .resp(CallOptionResp::Auto)
503            .build(),
504        args.iter()
505            .map(|v| v.as_str())
506            .collect::<Vec<&str>>()
507            .as_slice(),
508    );
509    let res = res.map_err(|e| {
510        RedisError::String(
511            e.to_utf8_string()
512                .unwrap_or("Failed converting error to utf8".into()),
513        )
514    })?;
515    Ok((&res).into())
516}
517
518pub fn module_config_set(
519    ctx: &Context,
520    args: Vec<RedisString>,
521    name: &str,
522) -> Result<RedisValue, RedisError> {
523    let mut args: Vec<String> = args
524        .into_iter()
525        .skip(1)
526        .enumerate()
527        .map(|(index, e)| {
528            if index % 2 == 0 {
529                format!("{}.{}", name, e.to_string_lossy())
530            } else {
531                e.to_string_lossy()
532            }
533        })
534        .collect();
535    args.insert(0, "set".into());
536    let res: CallResult = ctx.call_ext(
537        "config",
538        &CallOptionsBuilder::new()
539            .errors_as_replies()
540            .resp(CallOptionResp::Auto)
541            .build(),
542        args.iter()
543            .map(|v| v.as_str())
544            .collect::<Vec<&str>>()
545            .as_slice(),
546    );
547    let res = res.map_err(|e| {
548        RedisError::String(
549            e.to_utf8_string()
550                .unwrap_or("Failed converting error to utf8".into()),
551        )
552    })?;
553    Ok((&res).into())
554}