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::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;
10use std::any::TypeId;
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 { &*(privdata as *const ConfigrationPrivateData<ValkeyString, ValkeyGILGuard<ValkeyString>>) };
316            let valkey_gilguard = private_data.variable;
317            let ctx = &ConfigurationContext::new();
318            let value = valkey_gilguard.lock(ctx);
319            value.safe_clone(&Context::dummy()).inner
320        },
321        _ => {
322            // For non ValkeyString types we can go through the typical flow of getting the config
323            let private_data = unsafe { &*(privdata as *const ConfigrationPrivateData<ValkeyString, T>) };
324            private_data
325                .variable
326                .get(&ConfigurationContext::new())
327                .take()
328        }
329    }
330}
331
332pub fn register_string_configuration<T: ConfigurationValue<ValkeyString>>(
333    ctx: &Context,
334    name: &str,
335    variable: &'static T,
336    default: &str,
337    flags: ConfigurationFlags,
338    on_changed: Option<OnUpdatedCallback<T>>,
339    on_set: Option<OnSetCallback<T>>,
340) {
341    let name = CString::new(name).unwrap();
342    let default = CString::new(default).unwrap();
343    let config_private_data = ConfigrationPrivateData {
344        variable,
345        on_changed,
346        on_set,
347        phantom: PhantomData::<ValkeyString>,
348    };
349    unsafe {
350        raw::RedisModule_RegisterStringConfig.unwrap()(
351            ctx.ctx,
352            name.as_ptr(),
353            default.as_ptr(),
354            flags.bits(),
355            Some(string_configuration_get::<T>),
356            Some(string_configuration_set::<T>),
357            None,
358            Box::into_raw(Box::new(config_private_data)) as *mut c_void,
359        );
360    }
361}
362
363pub fn get_string_default_config_value<'a>(
364    args: &'a [ValkeyString],
365    name: &str,
366    default: &'a str,
367) -> Result<&'a str, ValkeyError> {
368    find_config_value(args, name).map_or(Ok(default), |arg| arg.try_as_str())
369}
370
371extern "C" fn bool_configuration_set<T: ConfigurationValue<bool> + 'static>(
372    name: *const c_char,
373    val: i32,
374    privdata: *mut c_void,
375    err: *mut *mut raw::RedisModuleString,
376) -> c_int {
377    let private_data = unsafe { &*(privdata as *const ConfigrationPrivateData<bool, T>) };
378    private_data.set_val(name, val != 0, err)
379}
380
381extern "C" fn bool_configuration_get<T: ConfigurationValue<bool> + 'static>(
382    _name: *const c_char,
383    privdata: *mut c_void,
384) -> c_int {
385    let private_data = unsafe { &*(privdata as *const ConfigrationPrivateData<bool, T>) };
386    private_data.get_val() as i32
387}
388
389pub fn register_bool_configuration<T: ConfigurationValue<bool>>(
390    ctx: &Context,
391    name: &str,
392    variable: &'static T,
393    default: bool,
394    flags: ConfigurationFlags,
395    on_changed: Option<OnUpdatedCallback<T>>,
396    on_set: Option<OnSetCallback<T>>,
397) {
398    let name = CString::new(name).unwrap();
399    let config_private_data = ConfigrationPrivateData {
400        variable,
401        on_changed,
402        on_set,
403        phantom: PhantomData::<bool>,
404    };
405    unsafe {
406        raw::RedisModule_RegisterBoolConfig.unwrap()(
407            ctx.ctx,
408            name.as_ptr(),
409            default as i32,
410            flags.bits(),
411            Some(bool_configuration_get::<T>),
412            Some(bool_configuration_set::<T>),
413            None,
414            Box::into_raw(Box::new(config_private_data)) as *mut c_void,
415        );
416    }
417}
418
419pub fn get_bool_default_config_value(
420    args: &[ValkeyString],
421    name: &str,
422    default: bool,
423) -> Result<bool, ValkeyError> {
424    find_config_value(args, name).map_or(Ok(default), |arg| Ok(arg.try_as_str()? == "yes"))
425}
426
427extern "C" fn enum_configuration_set<
428    G: EnumConfigurationValue,
429    T: ConfigurationValue<G> + 'static,
430>(
431    name: *const c_char,
432    val: i32,
433    privdata: *mut c_void,
434    err: *mut *mut raw::RedisModuleString,
435) -> c_int {
436    let private_data = unsafe { &*(privdata as *const ConfigrationPrivateData<G, T>) };
437    let val: Result<G, _> = val.try_into();
438    match val {
439        Ok(val) => private_data.set_val(name, val, err),
440        Err(e) => {
441            let error_msg = ValkeyString::create(None, e.to_string().as_str());
442            unsafe { *err = error_msg.take() };
443            raw::REDISMODULE_ERR as i32
444        }
445    }
446}
447
448extern "C" fn enum_configuration_get<
449    G: EnumConfigurationValue,
450    T: ConfigurationValue<G> + 'static,
451>(
452    _name: *const c_char,
453    privdata: *mut c_void,
454) -> c_int {
455    let private_data = unsafe { &*(privdata as *const ConfigrationPrivateData<G, T>) };
456    private_data.get_val().into()
457}
458
459pub fn register_enum_configuration<G: EnumConfigurationValue, T: ConfigurationValue<G>>(
460    ctx: &Context,
461    name: &str,
462    variable: &'static T,
463    default: G,
464    flags: ConfigurationFlags,
465    on_changed: Option<OnUpdatedCallback<T>>,
466    on_set: Option<OnSetCallback<T>>,
467) {
468    let name = CString::new(name).unwrap();
469    let (names, vals) = default.get_options();
470    assert_eq!(names.len(), vals.len());
471    let names: Vec<CString> = names
472        .into_iter()
473        .map(|v| CString::new(v).unwrap())
474        .collect();
475    let config_private_data = ConfigrationPrivateData {
476        variable,
477        on_changed,
478        on_set,
479        phantom: PhantomData::<G>,
480    };
481    unsafe {
482        raw::RedisModule_RegisterEnumConfig.unwrap()(
483            ctx.ctx,
484            name.as_ptr(),
485            default.into(),
486            flags.bits(),
487            names
488                .iter()
489                .map(|v| v.as_ptr())
490                .collect::<Vec<*const c_char>>()
491                .as_mut_ptr(),
492            vals.as_ptr(),
493            names.len() as i32,
494            Some(enum_configuration_get::<G, T>),
495            Some(enum_configuration_set::<G, T>),
496            None,
497            Box::into_raw(Box::new(config_private_data)) as *mut c_void,
498        );
499    }
500}
501
502pub fn get_enum_default_config_value<G: EnumConfigurationValue>(
503    args: &[ValkeyString],
504    name: &str,
505    default: G,
506) -> Result<G, ValkeyError> {
507    find_config_value(args, name).map_or(Ok(default.clone()), |arg| {
508        let (names, vals) = default.get_options();
509        let (index, _name) = names
510            .into_iter()
511            .enumerate()
512            .find(|(_index, item)| item.as_bytes().eq(arg.as_slice()))
513            .ok_or(ValkeyError::String(format!(
514                "Enum '{}' not exists",
515                arg.to_string_lossy()
516            )))?;
517        G::try_from(vals[index])
518    })
519}
520
521pub fn module_config_get(
522    ctx: &Context,
523    args: Vec<ValkeyString>,
524    name: &str,
525) -> Result<ValkeyValue, ValkeyError> {
526    let mut args: Vec<String> = args
527        .into_iter()
528        .skip(1)
529        .map(|e| format!("{}.{}", name, e.to_string_lossy()))
530        .collect();
531    args.insert(0, "get".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        ValkeyError::String(
545            e.to_utf8_string()
546                .unwrap_or("Failed converting error to utf8".into()),
547        )
548    })?;
549    Ok((&res).into())
550}
551
552pub fn module_config_set(
553    ctx: &Context,
554    args: Vec<ValkeyString>,
555    name: &str,
556) -> Result<ValkeyValue, ValkeyError> {
557    let mut args: Vec<String> = args
558        .into_iter()
559        .skip(1)
560        .enumerate()
561        .map(|(index, e)| {
562            if index % 2 == 0 {
563                format!("{}.{}", name, e.to_string_lossy())
564            } else {
565                e.to_string_lossy()
566            }
567        })
568        .collect();
569    args.insert(0, "set".into());
570    let res: CallResult = ctx.call_ext(
571        "config",
572        &CallOptionsBuilder::new()
573            .errors_as_replies()
574            .resp(CallOptionResp::Auto)
575            .build(),
576        args.iter()
577            .map(|v| v.as_str())
578            .collect::<Vec<&str>>()
579            .as_slice(),
580    );
581    let res = res.map_err(|e| {
582        ValkeyError::String(
583            e.to_utf8_string()
584                .unwrap_or("Failed converting error to utf8".into()),
585        )
586    })?;
587    Ok((&res).into())
588}