valkey_module/
configuration.rs

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