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;
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::ValkeyError;
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::ValkeyError::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 Valkey 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 ValkeyLockIndicator 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<(), ValkeyError>;
102}
103
104pub trait EnumConfigurationValue: TryFrom<i32, Error = ValkeyError> + Into<i32> + Clone {
105    fn get_options(&self) -> (Vec<String>, Vec<i32>);
106}
107
108impl<T: Clone> ConfigurationValue<T> for ValkeyGILGuard<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<(), ValkeyError> {
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<(), ValkeyError> {
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<(), ValkeyError> {
137        self.store(val, Ordering::Relaxed);
138        Ok(())
139    }
140}
141
142impl ConfigurationValue<ValkeyString> for ValkeyGILGuard<String> {
143    fn get(&self, ctx: &ConfigurationContext) -> ValkeyString {
144        let value = self.lock(ctx);
145        ValkeyString::create(None, value.as_str())
146    }
147    fn set(&self, ctx: &ConfigurationContext, val: ValkeyString) -> Result<(), ValkeyError> {
148        let mut value = self.lock(ctx);
149        *value = val.try_as_str()?.to_string();
150        Ok(())
151    }
152}
153
154impl ConfigurationValue<ValkeyString> for Mutex<String> {
155    fn get(&self, _ctx: &ConfigurationContext) -> ValkeyString {
156        let value = self.lock().unwrap();
157        ValkeyString::create(None, value.as_str())
158    }
159    fn set(&self, _ctx: &ConfigurationContext, val: ValkeyString) -> Result<(), ValkeyError> {
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<(), ValkeyError> {
171        self.store(val, Ordering::Relaxed);
172        Ok(())
173    }
174}
175
176type OnUpdatedCallback<T> = Box<dyn Fn(&ConfigurationContext, &str, &'static T)>;
177
178type OnSetCallback<T> =
179    Box<dyn Fn(&ConfigurationContext, &str, &'static T) -> Result<(), ValkeyError>>;
180
181struct ConfigrationPrivateData<G, T: ConfigurationValue<G> + 'static> {
182    variable: &'static T,
183    on_changed: Option<OnUpdatedCallback<T>>,
184    on_set: Option<OnSetCallback<T>>,
185    phantom: PhantomData<G>,
186}
187
188impl<G, T: ConfigurationValue<G> + 'static> ConfigrationPrivateData<G, T> {
189    fn set_val(&self, name: *const c_char, val: G, err: *mut *mut raw::RedisModuleString) -> c_int {
190        // we know the GIL is held so it is safe to use Context::dummy().
191        let configuration_ctx = ConfigurationContext::new();
192        if let Err(e) = self.variable.set(&configuration_ctx, val) {
193            let error_msg = ValkeyString::create(None, e.to_string().as_str());
194            unsafe { *err = error_msg.take() };
195            return raw::REDISMODULE_ERR as i32;
196        }
197        let c_str_name = unsafe { CStr::from_ptr(name) };
198        if let Some(v) = self.on_set.as_ref() {
199            let result = v(
200                &configuration_ctx,
201                c_str_name.to_str().unwrap(),
202                self.variable,
203            );
204            if let Err(e) = result {
205                let error_msg = ValkeyString::create(None, e.to_string().as_str());
206                unsafe { *err = error_msg.take() };
207                return raw::REDISMODULE_ERR as i32;
208            }
209        }
210        if let Some(v) = self.on_changed.as_ref() {
211            v(
212                &configuration_ctx,
213                c_str_name.to_str().unwrap(),
214                self.variable,
215            );
216        }
217        raw::REDISMODULE_OK as i32
218    }
219
220    fn get_val(&self) -> G {
221        self.variable.get(&ConfigurationContext::new())
222    }
223}
224
225extern "C" fn i64_configuration_set<T: ConfigurationValue<i64> + 'static>(
226    name: *const c_char,
227    val: c_longlong,
228    privdata: *mut c_void,
229    err: *mut *mut raw::RedisModuleString,
230) -> c_int {
231    let private_data = unsafe { &*(privdata as *const ConfigrationPrivateData<i64, T>) };
232    private_data.set_val(name, val, err)
233}
234
235extern "C" fn i64_configuration_get<T: ConfigurationValue<i64> + 'static>(
236    _name: *const c_char,
237    privdata: *mut c_void,
238) -> c_longlong {
239    let private_data = unsafe { &*(privdata as *const ConfigrationPrivateData<i64, T>) };
240    private_data.get_val()
241}
242
243pub fn register_i64_configuration<T: ConfigurationValue<i64>>(
244    ctx: &Context,
245    name: &str,
246    variable: &'static T,
247    default: i64,
248    min: i64,
249    max: i64,
250    flags: ConfigurationFlags,
251    on_changed: Option<OnUpdatedCallback<T>>,
252    on_set: Option<OnSetCallback<T>>,
253) {
254    let name = CString::new(name).unwrap();
255    let config_private_data = ConfigrationPrivateData {
256        variable,
257        on_changed,
258        on_set,
259        phantom: PhantomData::<i64>,
260    };
261    unsafe {
262        raw::RedisModule_RegisterNumericConfig.unwrap()(
263            ctx.ctx,
264            name.as_ptr(),
265            default,
266            flags.bits(),
267            min,
268            max,
269            Some(i64_configuration_get::<T>),
270            Some(i64_configuration_set::<T>),
271            None,
272            Box::into_raw(Box::new(config_private_data)) as *mut c_void,
273        );
274    }
275}
276
277fn find_config_value<'a>(args: &'a [ValkeyString], name: &str) -> Option<&'a ValkeyString> {
278    args.iter()
279        .skip_while(|item| !item.as_slice().eq(name.as_bytes()))
280        .nth(1)
281}
282
283pub fn get_i64_default_config_value(
284    args: &[ValkeyString],
285    name: &str,
286    default: i64,
287) -> Result<i64, ValkeyError> {
288    find_config_value(args, name).map_or(Ok(default), |arg| {
289        arg.try_as_str()?
290            .parse::<i64>()
291            .map_err(|e| ValkeyError::String(e.to_string()))
292    })
293}
294
295extern "C" fn string_configuration_set<T: ConfigurationValue<ValkeyString> + 'static>(
296    name: *const c_char,
297    val: *mut raw::RedisModuleString,
298    privdata: *mut c_void,
299    err: *mut *mut raw::RedisModuleString,
300) -> c_int {
301    let new_val = ValkeyString::new(None, val);
302    let private_data = unsafe { &*(privdata as *const ConfigrationPrivateData<ValkeyString, T>) };
303    private_data.set_val(name, new_val, err)
304}
305
306extern "C" fn string_configuration_get<T: ConfigurationValue<ValkeyString> + 'static>(
307    _name: *const c_char,
308    privdata: *mut c_void,
309) -> *mut raw::RedisModuleString {
310    let private_data = unsafe { &*(privdata as *const ConfigrationPrivateData<ValkeyString, T>) };
311    // we know the GIL is held so it is safe to use Context::dummy().
312    private_data
313        .variable
314        .get(&ConfigurationContext::new())
315        .take()
316}
317
318pub fn register_string_configuration<T: ConfigurationValue<ValkeyString>>(
319    ctx: &Context,
320    name: &str,
321    variable: &'static T,
322    default: &str,
323    flags: ConfigurationFlags,
324    on_changed: Option<OnUpdatedCallback<T>>,
325    on_set: Option<OnSetCallback<T>>,
326) {
327    let name = CString::new(name).unwrap();
328    let default = CString::new(default).unwrap();
329    let config_private_data = ConfigrationPrivateData {
330        variable,
331        on_changed,
332        on_set,
333        phantom: PhantomData::<ValkeyString>,
334    };
335    unsafe {
336        raw::RedisModule_RegisterStringConfig.unwrap()(
337            ctx.ctx,
338            name.as_ptr(),
339            default.as_ptr(),
340            flags.bits(),
341            Some(string_configuration_get::<T>),
342            Some(string_configuration_set::<T>),
343            None,
344            Box::into_raw(Box::new(config_private_data)) as *mut c_void,
345        );
346    }
347}
348
349pub fn get_string_default_config_value<'a>(
350    args: &'a [ValkeyString],
351    name: &str,
352    default: &'a str,
353) -> Result<&'a str, ValkeyError> {
354    find_config_value(args, name).map_or(Ok(default), |arg| arg.try_as_str())
355}
356
357extern "C" fn bool_configuration_set<T: ConfigurationValue<bool> + 'static>(
358    name: *const c_char,
359    val: i32,
360    privdata: *mut c_void,
361    err: *mut *mut raw::RedisModuleString,
362) -> c_int {
363    let private_data = unsafe { &*(privdata as *const ConfigrationPrivateData<bool, T>) };
364    private_data.set_val(name, val != 0, err)
365}
366
367extern "C" fn bool_configuration_get<T: ConfigurationValue<bool> + 'static>(
368    _name: *const c_char,
369    privdata: *mut c_void,
370) -> c_int {
371    let private_data = unsafe { &*(privdata as *const ConfigrationPrivateData<bool, T>) };
372    private_data.get_val() as i32
373}
374
375pub fn register_bool_configuration<T: ConfigurationValue<bool>>(
376    ctx: &Context,
377    name: &str,
378    variable: &'static T,
379    default: bool,
380    flags: ConfigurationFlags,
381    on_changed: Option<OnUpdatedCallback<T>>,
382    on_set: Option<OnSetCallback<T>>,
383) {
384    let name = CString::new(name).unwrap();
385    let config_private_data = ConfigrationPrivateData {
386        variable,
387        on_changed,
388        on_set,
389        phantom: PhantomData::<bool>,
390    };
391    unsafe {
392        raw::RedisModule_RegisterBoolConfig.unwrap()(
393            ctx.ctx,
394            name.as_ptr(),
395            default as i32,
396            flags.bits(),
397            Some(bool_configuration_get::<T>),
398            Some(bool_configuration_set::<T>),
399            None,
400            Box::into_raw(Box::new(config_private_data)) as *mut c_void,
401        );
402    }
403}
404
405pub fn get_bool_default_config_value(
406    args: &[ValkeyString],
407    name: &str,
408    default: bool,
409) -> Result<bool, ValkeyError> {
410    find_config_value(args, name).map_or(Ok(default), |arg| Ok(arg.try_as_str()? == "yes"))
411}
412
413extern "C" fn enum_configuration_set<
414    G: EnumConfigurationValue,
415    T: ConfigurationValue<G> + 'static,
416>(
417    name: *const c_char,
418    val: i32,
419    privdata: *mut c_void,
420    err: *mut *mut raw::RedisModuleString,
421) -> c_int {
422    let private_data = unsafe { &*(privdata as *const ConfigrationPrivateData<G, T>) };
423    let val: Result<G, _> = val.try_into();
424    match val {
425        Ok(val) => private_data.set_val(name, val, err),
426        Err(e) => {
427            let error_msg = ValkeyString::create(None, e.to_string().as_str());
428            unsafe { *err = error_msg.take() };
429            raw::REDISMODULE_ERR as i32
430        }
431    }
432}
433
434extern "C" fn enum_configuration_get<
435    G: EnumConfigurationValue,
436    T: ConfigurationValue<G> + 'static,
437>(
438    _name: *const c_char,
439    privdata: *mut c_void,
440) -> c_int {
441    let private_data = unsafe { &*(privdata as *const ConfigrationPrivateData<G, T>) };
442    private_data.get_val().into()
443}
444
445pub fn register_enum_configuration<G: EnumConfigurationValue, T: ConfigurationValue<G>>(
446    ctx: &Context,
447    name: &str,
448    variable: &'static T,
449    default: G,
450    flags: ConfigurationFlags,
451    on_changed: Option<OnUpdatedCallback<T>>,
452    on_set: Option<OnSetCallback<T>>,
453) {
454    let name = CString::new(name).unwrap();
455    let (names, vals) = default.get_options();
456    assert_eq!(names.len(), vals.len());
457    let names: Vec<CString> = names
458        .into_iter()
459        .map(|v| CString::new(v).unwrap())
460        .collect();
461    let config_private_data = ConfigrationPrivateData {
462        variable,
463        on_changed,
464        on_set,
465        phantom: PhantomData::<G>,
466    };
467    unsafe {
468        raw::RedisModule_RegisterEnumConfig.unwrap()(
469            ctx.ctx,
470            name.as_ptr(),
471            default.into(),
472            flags.bits(),
473            names
474                .iter()
475                .map(|v| v.as_ptr())
476                .collect::<Vec<*const c_char>>()
477                .as_mut_ptr(),
478            vals.as_ptr(),
479            names.len() as i32,
480            Some(enum_configuration_get::<G, T>),
481            Some(enum_configuration_set::<G, T>),
482            None,
483            Box::into_raw(Box::new(config_private_data)) as *mut c_void,
484        );
485    }
486}
487
488pub fn get_enum_default_config_value<G: EnumConfigurationValue>(
489    args: &[ValkeyString],
490    name: &str,
491    default: G,
492) -> Result<G, ValkeyError> {
493    find_config_value(args, name).map_or(Ok(default.clone()), |arg| {
494        let (names, vals) = default.get_options();
495        let (index, _name) = names
496            .into_iter()
497            .enumerate()
498            .find(|(_index, item)| item.as_bytes().eq(arg.as_slice()))
499            .ok_or(ValkeyError::String(format!(
500                "Enum '{}' not exists",
501                arg.to_string_lossy()
502            )))?;
503        G::try_from(vals[index])
504    })
505}
506
507pub fn module_config_get(
508    ctx: &Context,
509    args: Vec<ValkeyString>,
510    name: &str,
511) -> Result<ValkeyValue, ValkeyError> {
512    let mut args: Vec<String> = args
513        .into_iter()
514        .skip(1)
515        .map(|e| format!("{}.{}", name, e.to_string_lossy()))
516        .collect();
517    args.insert(0, "get".into());
518    let res: CallResult = ctx.call_ext(
519        "config",
520        &CallOptionsBuilder::new()
521            .errors_as_replies()
522            .resp(CallOptionResp::Auto)
523            .build(),
524        args.iter()
525            .map(|v| v.as_str())
526            .collect::<Vec<&str>>()
527            .as_slice(),
528    );
529    let res = res.map_err(|e| {
530        ValkeyError::String(
531            e.to_utf8_string()
532                .unwrap_or("Failed converting error to utf8".into()),
533        )
534    })?;
535    Ok((&res).into())
536}
537
538pub fn module_config_set(
539    ctx: &Context,
540    args: Vec<ValkeyString>,
541    name: &str,
542) -> Result<ValkeyValue, ValkeyError> {
543    let mut args: Vec<String> = args
544        .into_iter()
545        .skip(1)
546        .enumerate()
547        .map(|(index, e)| {
548            if index % 2 == 0 {
549                format!("{}.{}", name, e.to_string_lossy())
550            } else {
551                e.to_string_lossy()
552            }
553        })
554        .collect();
555    args.insert(0, "set".into());
556    let res: CallResult = ctx.call_ext(
557        "config",
558        &CallOptionsBuilder::new()
559            .errors_as_replies()
560            .resp(CallOptionResp::Auto)
561            .build(),
562        args.iter()
563            .map(|v| v.as_str())
564            .collect::<Vec<&str>>()
565            .as_slice(),
566    );
567    let res = res.map_err(|e| {
568        ValkeyError::String(
569            e.to_utf8_string()
570                .unwrap_or("Failed converting error to utf8".into()),
571        )
572    })?;
573    Ok((&res).into())
574}