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