From deeb441ba1e4b86e303e7a97705802aee21299fe Mon Sep 17 00:00:00 2001 From: "Meir Shpilraien (Spielrein)" Date: Sun, 26 Mar 2023 09:31:54 +0300 Subject: [PATCH] Added configuration API. (#297) The PR adds the ability to add module configuration as using configuration API introduced on Redis 7: https://github.com/redis/redis/pull/10285 The configuration is added optionally on `redis_module!` macro under `configurations` list that can contains the following subsections: * `i64` configuration: A list of `i64` configuration in the following format: `[, , , , , , ]` * `string` configuration: A list of `string` configuration in the following format: `[, , , , ]` * `bool` configuration: A list of `bool` configuration in the following format: `[, , , , ]` * `enum` configuration: A list of `enum` configuration in the following format: `[, , , , ]` An example of all the 4 options can be found under `example/configuration.rs`. Notice that `enum` configuration is of special type and require provide an `enum` that implements the following traits: `TryFrom`, `From<$name>`, `EnumConfigurationValue`, `Clone`. User can use `enum_configuration` macro to easily added those implementation on a given `enum`. In addition, it is also possible to tell redismodule-rs to look at the module arguments as if they were configuration using `module_args_as_configuration: true/false` option. Usage examample: ```rust /// A macro to easily creating an enum that can be used as an enum configuration enum_configuration! { enum EnumConfiguration { Val1 = 1, Val2 = 2, } } /// Static variable that can be used as configuration and will be automatically set when `config set` command is used. /// All the variables must be somehow thread safe protected, either as atomic variable, `RedisGILGuard` or `Mutex`. lazy_static! { static ref CONFIGURATION_I64: RedisGILGuard = RedisGILGuard::default(); static ref CONFIGURATION_ATOMIC_I64: AtomicI64 = AtomicI64::new(1); static ref CONFIGURATION_REDIS_STRING: RedisGILGuard = RedisGILGuard::new(RedisString::create(None, "default")); static ref CONFIGURATION_STRING: RedisGILGuard = RedisGILGuard::new("default".into()); static ref CONFIGURATION_MUTEX_STRING: Mutex = Mutex::new("default".into()); static ref CONFIGURATION_ATOMIC_BOOL: AtomicBool = AtomicBool::default(); static ref CONFIGURATION_BOOL: RedisGILGuard = RedisGILGuard::default(); static ref CONFIGURATION_ENUM: RedisGILGuard = RedisGILGuard::new(EnumConfiguration::Val1); static ref CONFIGURATION_MUTEX_ENUM: Mutex = Mutex::new(EnumConfiguration::Val1); } /// This function will be called when a configuration change is done and will count the total number of changes. fn num_changes(ctx: &Context, _: Vec) -> RedisResult { let val = NUM_OF_CONFIGERATION_CHANGES.lock(ctx); Ok(RedisValue::Integer(*val)) } /// The module initialisation macro, gets the configuration variable and some extra data (such as configuration name and /// default value) and pass it to Redis so Redis will set them and return their values on `config set` and `config get` respectively. redis_module! { name: "configuration", version: 1, data_types: [], commands: [ ["configuration.num_changes", num_changes, "", 0, 0, 0], ], configurations: [ i64: [ ["i64", &*CONFIGURATION_I64, 10, 0, 1000, ConfigurationFlags::DEFAULT, Some(Box::new(on_configuration_changed))], ["atomic_i64", &*CONFIGURATION_ATOMIC_I64, 10, 0, 1000, ConfigurationFlags::DEFAULT, Some(Box::new(on_configuration_changed))], ], string: [ ["redis_string", &*CONFIGURATION_REDIS_STRING, "default", ConfigurationFlags::DEFAULT, Some(Box::new(on_configuration_changed))], ["string", &*CONFIGURATION_STRING, "default", ConfigurationFlags::DEFAULT, Some(Box::new(on_configuration_changed::))], ["mutex_string", &*CONFIGURATION_MUTEX_STRING, "default", ConfigurationFlags::DEFAULT, Some(Box::new(on_configuration_changed::))], ], bool: [ ["atomic_bool", &*CONFIGURATION_ATOMIC_BOOL, true, ConfigurationFlags::DEFAULT, Some(Box::new(on_configuration_changed))], ["bool", &*CONFIGURATION_BOOL, true, ConfigurationFlags::DEFAULT, Some(Box::new(on_configuration_changed))], ], enum: [ ["enum", &*CONFIGURATION_ENUM, EnumConfiguration::Val1, ConfigurationFlags::DEFAULT, Some(Box::new(on_configuration_changed))], ["enum_mutex", &*CONFIGURATION_MUTEX_ENUM, EnumConfiguration::Val1, ConfigurationFlags::DEFAULT, Some(Box::new(on_configuration_changed))], ], module_args_as_configuration: true, // Indication that we also want to read module arguments as configuration. ] } ``` --- Cargo.toml | 4 + examples/configuration.rs | 82 +++++++ examples/events.rs | 3 +- examples/threads.rs | 2 +- src/configuration.rs | 446 +++++++++++++++++++++++++++++++++++++ src/context/info.rs | 3 +- src/context/keys_cursor.rs | 3 +- src/context/mod.rs | 6 +- src/context/thread_safe.rs | 58 ++++- src/key.rs | 11 +- src/lib.rs | 3 + src/logging.rs | 3 - src/macros.rs | 70 ++++++ src/raw.rs | 9 +- src/redismodule.rs | 14 +- tests/integration.rs | 73 ++++++ 16 files changed, 758 insertions(+), 32 deletions(-) create mode 100644 examples/configuration.rs create mode 100644 src/configuration.rs diff --git a/Cargo.toml b/Cargo.toml index f837e36e..1615ded4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,10 @@ crate-type = ["cdylib"] name = "string" crate-type = ["cdylib"] +[[example]] +name = "configuration" +crate-type = ["cdylib"] + [[example]] name = "acl" crate-type = ["cdylib"] diff --git a/examples/configuration.rs b/examples/configuration.rs new file mode 100644 index 00000000..949bd5f2 --- /dev/null +++ b/examples/configuration.rs @@ -0,0 +1,82 @@ +#[macro_use] +extern crate redis_module; + +use std::sync::{ + atomic::{AtomicBool, AtomicI64}, + Mutex, +}; + +use lazy_static::lazy_static; +use redis_module::{ + configuration::{ConfigurationContext, ConfigurationFlags}, + ConfigurationValue, Context, EnumConfigurationValue, RedisGILGuard, RedisResult, RedisString, + RedisValue, +}; + +enum_configuration! { + enum EnumConfiguration { + Val1 = 1, + Val2 = 2, + } +} + +lazy_static! { + static ref NUM_OF_CONFIGURATION_CHANGES: RedisGILGuard = RedisGILGuard::default(); + static ref CONFIGURATION_I64: RedisGILGuard = RedisGILGuard::default(); + static ref CONFIGURATION_ATOMIC_I64: AtomicI64 = AtomicI64::new(1); + static ref CONFIGURATION_REDIS_STRING: RedisGILGuard = + RedisGILGuard::new(RedisString::create(None, "default")); + static ref CONFIGURATION_STRING: RedisGILGuard = RedisGILGuard::new("default".into()); + static ref CONFIGURATION_MUTEX_STRING: Mutex = Mutex::new("default".into()); + static ref CONFIGURATION_ATOMIC_BOOL: AtomicBool = AtomicBool::default(); + static ref CONFIGURATION_BOOL: RedisGILGuard = RedisGILGuard::default(); + static ref CONFIGURATION_ENUM: RedisGILGuard = + RedisGILGuard::new(EnumConfiguration::Val1); + static ref CONFIGURATION_MUTEX_ENUM: Mutex = + Mutex::new(EnumConfiguration::Val1); +} + +fn on_configuration_changed>( + config_ctx: &ConfigurationContext, + _name: &str, + _val: &'static T, +) { + let mut val = NUM_OF_CONFIGURATION_CHANGES.lock(config_ctx); + *val += 1 +} + +fn num_changes(ctx: &Context, _: Vec) -> RedisResult { + let val = NUM_OF_CONFIGURATION_CHANGES.lock(ctx); + Ok(RedisValue::Integer(*val)) +} + +////////////////////////////////////////////////////// + +redis_module! { + name: "configuration", + version: 1, + data_types: [], + commands: [ + ["configuration.num_changes", num_changes, "", 0, 0, 0], + ], + configurations: [ + i64: [ + ["i64", &*CONFIGURATION_I64, 10, 0, 1000, ConfigurationFlags::DEFAULT, Some(Box::new(on_configuration_changed))], + ["atomic_i64", &*CONFIGURATION_ATOMIC_I64, 10, 0, 1000, ConfigurationFlags::DEFAULT, Some(Box::new(on_configuration_changed))], + ], + string: [ + ["redis_string", &*CONFIGURATION_REDIS_STRING, "default", ConfigurationFlags::DEFAULT, Some(Box::new(on_configuration_changed))], + ["string", &*CONFIGURATION_STRING, "default", ConfigurationFlags::DEFAULT, Some(Box::new(on_configuration_changed::))], + ["mutex_string", &*CONFIGURATION_MUTEX_STRING, "default", ConfigurationFlags::DEFAULT, Some(Box::new(on_configuration_changed::))], + ], + bool: [ + ["atomic_bool", &*CONFIGURATION_ATOMIC_BOOL, true, ConfigurationFlags::DEFAULT, Some(Box::new(on_configuration_changed))], + ["bool", &*CONFIGURATION_BOOL, true, ConfigurationFlags::DEFAULT, Some(Box::new(on_configuration_changed))], + ], + enum: [ + ["enum", &*CONFIGURATION_ENUM, EnumConfiguration::Val1, ConfigurationFlags::DEFAULT, Some(Box::new(on_configuration_changed))], + ["enum_mutex", &*CONFIGURATION_MUTEX_ENUM, EnumConfiguration::Val1, ConfigurationFlags::DEFAULT, Some(Box::new(on_configuration_changed))], + ], + module_args_as_configuration: true, + ] +} diff --git a/examples/events.rs b/examples/events.rs index e09da95b..80ee4df5 100644 --- a/examples/events.rs +++ b/examples/events.rs @@ -4,6 +4,7 @@ extern crate redis_module; use redis_module::{ Context, NotifyEvent, RedisError, RedisResult, RedisString, RedisValue, Status, }; +use std::ptr::NonNull; use std::sync::atomic::{AtomicI64, Ordering}; static NUM_KEY_MISSES: AtomicI64 = AtomicI64::new(0); @@ -25,7 +26,7 @@ fn event_send(ctx: &Context, args: Vec) -> RedisResult { return Err(RedisError::WrongArity); } - let key_name = RedisString::create(ctx.ctx, "mykey"); + let key_name = RedisString::create(NonNull::new(ctx.ctx), "mykey"); let status = ctx.notify_keyspace_event(NotifyEvent::GENERIC, "events.send", &key_name); match status { Status::Ok => Ok("Event sent".into()), diff --git a/examples/threads.rs b/examples/threads.rs index 772fd8f3..2524011e 100644 --- a/examples/threads.rs +++ b/examples/threads.rs @@ -31,7 +31,7 @@ struct StaticData { } lazy_static! { - static ref STATIC_DATA: RedisGILGuard = RedisGILGuard::default(); + static ref STATIC_DATA: RedisGILGuard = RedisGILGuard::new(StaticData::default()); } fn set_static_data(ctx: &Context, args: Vec) -> RedisResult { diff --git a/src/configuration.rs b/src/configuration.rs new file mode 100644 index 00000000..06174430 --- /dev/null +++ b/src/configuration.rs @@ -0,0 +1,446 @@ +use crate::context::thread_safe::RedisLockIndicator; +use crate::{raw, RedisGILGuard}; +use crate::{Context, RedisError, RedisString}; +use bitflags::bitflags; +use std::ffi::{c_char, c_int, c_longlong, c_void, CStr, CString}; +use std::marker::PhantomData; +use std::sync::atomic::{AtomicBool, AtomicI64, Ordering}; +use std::sync::Mutex; + +bitflags! { + /// Configuration options + pub struct ConfigurationFlags : u32 { + /// The default flags for a config. This creates a config that can be modified after startup. + const DEFAULT = raw::REDISMODULE_CONFIG_DEFAULT; + + /// This config can only be provided loading time. + const IMMUTABLE = raw::REDISMODULE_CONFIG_IMMUTABLE; + + /// The value stored in this config is redacted from all logging. + const SENSITIVE = raw::REDISMODULE_CONFIG_SENSITIVE; + + /// The name is hidden from `CONFIG GET` with pattern matching. + const HIDDEN = raw::REDISMODULE_CONFIG_HIDDEN; + + /// This config will be only be modifiable based off the value of enable-protected-configs. + const PROTECTED = raw::REDISMODULE_CONFIG_PROTECTED; + + /// This config is not modifiable while the server is loading data. + const DENY_LOADING = raw::REDISMODULE_CONFIG_DENY_LOADING; + + /// For numeric configs, this config will convert data unit notations into their byte equivalent. + const MEMORY = raw::REDISMODULE_CONFIG_MEMORY; + + /// For enum configs, this config will allow multiple entries to be combined as bit flags. + const BITFLAGS = raw::REDISMODULE_CONFIG_BITFLAGS; + } +} + +#[macro_export] +macro_rules! enum_configuration { + ($(#[$meta:meta])* $vis:vis enum $name:ident { + $($(#[$vmeta:meta])* $vname:ident = $val:expr,)* + }) => { + $(#[$meta])* + $vis enum $name { + $($(#[$vmeta])* $vname = $val,)* + } + + impl std::convert::TryFrom for $name { + type Error = $crate::RedisError; + + fn try_from(v: i32) -> Result { + match v { + $(x if x == $name::$vname as i32 => Ok($name::$vname),)* + _ => Err($crate::RedisError::Str("Value is not supported")), + } + } + } + + impl std::convert::From<$name> for i32 { + fn from(val: $name) -> Self { + val as i32 + } + } + + impl EnumConfigurationValue for $name { + fn get_options(&self) -> (Vec, Vec) { + (vec![$(stringify!($vname).to_string(),)*], vec![$($val,)*]) + } + } + + impl Clone for $name { + fn clone(&self) -> Self { + match self { + $($name::$vname => $name::$vname,)* + } + } + } + } +} + +/// [`ConfigurationContext`] is used as a special context that indicate that we are +/// running with the Redis GIL is held but we should not perform all the regular +/// operation we can perfrom on the regular Context. +pub struct ConfigurationContext { + _dummy: usize, // We set some none public vairable here so user will not be able to construct such object +} + +impl ConfigurationContext { + fn new() -> ConfigurationContext { + ConfigurationContext { _dummy: 0 } + } +} + +unsafe impl RedisLockIndicator for ConfigurationContext {} + +pub trait ConfigurationValue: Sync + Send { + fn get(&self, ctx: &ConfigurationContext) -> T; + fn set(&self, ctx: &ConfigurationContext, val: T) -> Result<(), RedisError>; +} + +pub trait EnumConfigurationValue: TryFrom + Into + Clone { + fn get_options(&self) -> (Vec, Vec); +} + +impl ConfigurationValue for RedisGILGuard { + fn get(&self, ctx: &ConfigurationContext) -> T { + let value = self.lock(ctx); + value.clone() + } + fn set(&self, ctx: &ConfigurationContext, val: T) -> Result<(), RedisError> { + let mut value = self.lock(ctx); + *value = val; + Ok(()) + } +} + +impl ConfigurationValue for Mutex { + fn get(&self, _ctx: &ConfigurationContext) -> T { + let value = self.lock().unwrap(); + value.clone() + } + fn set(&self, _ctx: &ConfigurationContext, val: T) -> Result<(), RedisError> { + let mut value = self.lock().unwrap(); + *value = val; + Ok(()) + } +} + +impl ConfigurationValue for AtomicI64 { + fn get(&self, _ctx: &ConfigurationContext) -> i64 { + self.load(Ordering::Relaxed) + } + fn set(&self, _ctx: &ConfigurationContext, val: i64) -> Result<(), RedisError> { + self.store(val, Ordering::Relaxed); + Ok(()) + } +} + +impl ConfigurationValue for RedisGILGuard { + fn get(&self, ctx: &ConfigurationContext) -> RedisString { + let value = self.lock(ctx); + RedisString::create(None, &value) + } + fn set(&self, ctx: &ConfigurationContext, val: RedisString) -> Result<(), RedisError> { + let mut value = self.lock(ctx); + *value = val.try_as_str()?.to_string(); + Ok(()) + } +} + +impl ConfigurationValue for Mutex { + fn get(&self, _ctx: &ConfigurationContext) -> RedisString { + let value = self.lock().unwrap(); + RedisString::create(None, &value) + } + fn set(&self, _ctx: &ConfigurationContext, val: RedisString) -> Result<(), RedisError> { + let mut value = self.lock().unwrap(); + *value = val.try_as_str()?.to_string(); + Ok(()) + } +} + +impl ConfigurationValue for AtomicBool { + fn get(&self, _ctx: &ConfigurationContext) -> bool { + self.load(Ordering::Relaxed) + } + fn set(&self, _ctx: &ConfigurationContext, val: bool) -> Result<(), RedisError> { + self.store(val, Ordering::Relaxed); + Ok(()) + } +} + +type OnUpdatedCallback = Box; + +struct ConfigrationPrivateData + 'static> { + variable: &'static T, + on_changed: Option>, + phantom: PhantomData, +} + +impl + 'static> ConfigrationPrivateData { + fn set_val(&self, name: *const c_char, val: G, err: *mut *mut raw::RedisModuleString) -> c_int { + // we know the GIL is held so it is safe to use Context::dummy(). + let configuration_ctx = ConfigurationContext::new(); + if let Err(e) = self.variable.set(&configuration_ctx, val) { + let error_msg = RedisString::create(None, &e.to_string()); + unsafe { *err = error_msg.take() }; + return raw::REDISMODULE_ERR as i32; + } + let c_str_name = unsafe { CStr::from_ptr(name) }; + self.on_changed.as_ref().map(|v| { + v( + &configuration_ctx, + c_str_name.to_str().unwrap(), + self.variable, + ) + }); + raw::REDISMODULE_OK as i32 + } + + fn get_val(&self) -> G { + self.variable.get(&ConfigurationContext::new()) + } +} + +extern "C" fn i64_configuration_set + 'static>( + name: *const c_char, + val: c_longlong, + privdata: *mut c_void, + err: *mut *mut raw::RedisModuleString, +) -> c_int { + let private_data = unsafe { &*(privdata as *const ConfigrationPrivateData) }; + private_data.set_val(name, val, err) +} + +extern "C" fn i64_configuration_get + 'static>( + _name: *const c_char, + privdata: *mut c_void, +) -> c_longlong { + let private_data = unsafe { &*(privdata as *const ConfigrationPrivateData) }; + private_data.get_val() +} + +pub fn register_i64_configuration>( + ctx: &Context, + name: &str, + variable: &'static T, + default: i64, + min: i64, + max: i64, + flags: ConfigurationFlags, + on_changed: Option>, +) { + let name = CString::new(name).unwrap(); + let config_private_data = ConfigrationPrivateData { + variable: variable, + on_changed: on_changed, + phantom: PhantomData::, + }; + unsafe { + raw::RedisModule_RegisterNumericConfig.unwrap()( + ctx.ctx, + name.as_ptr(), + default, + flags.bits() as u32, + min, + max, + Some(i64_configuration_get::), + Some(i64_configuration_set::), + None, + Box::into_raw(Box::new(config_private_data)) as *mut c_void, + ); + } +} + +extern "C" fn string_configuration_set + 'static>( + name: *const c_char, + val: *mut raw::RedisModuleString, + privdata: *mut c_void, + err: *mut *mut raw::RedisModuleString, +) -> c_int { + let new_val = RedisString::new(None, val); + let private_data = unsafe { &*(privdata as *const ConfigrationPrivateData) }; + private_data.set_val(name, new_val, err) +} + +extern "C" fn string_configuration_get + 'static>( + _name: *const c_char, + privdata: *mut c_void, +) -> *mut raw::RedisModuleString { + let private_data = unsafe { &*(privdata as *const ConfigrationPrivateData) }; + // we know the GIL is held so it is safe to use Context::dummy(). + private_data + .variable + .get(&ConfigurationContext::new()) + .take() +} + +pub fn register_string_configuration>( + ctx: &Context, + name: &str, + variable: &'static T, + default: &str, + flags: ConfigurationFlags, + on_changed: Option>, +) { + let name = CString::new(name).unwrap(); + let default = CString::new(default).unwrap(); + let config_private_data = ConfigrationPrivateData { + variable: variable, + on_changed: on_changed, + phantom: PhantomData::, + }; + unsafe { + raw::RedisModule_RegisterStringConfig.unwrap()( + ctx.ctx, + name.as_ptr(), + default.as_ptr(), + flags.bits() as u32, + Some(string_configuration_get::), + Some(string_configuration_set::), + None, + Box::into_raw(Box::new(config_private_data)) as *mut c_void, + ); + } +} + +extern "C" fn bool_configuration_set + 'static>( + name: *const c_char, + val: i32, + privdata: *mut c_void, + err: *mut *mut raw::RedisModuleString, +) -> c_int { + let private_data = unsafe { &*(privdata as *const ConfigrationPrivateData) }; + private_data.set_val(name, val != 0, err) +} + +extern "C" fn bool_configuration_get + 'static>( + _name: *const c_char, + privdata: *mut c_void, +) -> c_int { + let private_data = unsafe { &*(privdata as *const ConfigrationPrivateData) }; + private_data.get_val() as i32 +} + +pub fn register_bool_configuration>( + ctx: &Context, + name: &str, + variable: &'static T, + default: bool, + flags: ConfigurationFlags, + on_changed: Option>, +) { + let name = CString::new(name).unwrap(); + let config_private_data = ConfigrationPrivateData { + variable: variable, + on_changed: on_changed, + phantom: PhantomData::, + }; + unsafe { + raw::RedisModule_RegisterBoolConfig.unwrap()( + ctx.ctx, + name.as_ptr(), + default as i32, + flags.bits() as u32, + Some(bool_configuration_get::), + Some(bool_configuration_set::), + None, + Box::into_raw(Box::new(config_private_data)) as *mut c_void, + ); + } +} + +extern "C" fn enum_configuration_set< + G: EnumConfigurationValue, + T: ConfigurationValue + 'static, +>( + name: *const c_char, + val: i32, + privdata: *mut c_void, + err: *mut *mut raw::RedisModuleString, +) -> c_int { + let private_data = unsafe { &*(privdata as *const ConfigrationPrivateData) }; + let val: Result = val.try_into(); + match val { + Ok(val) => private_data.set_val(name, val, err), + Err(e) => { + let error_msg = RedisString::create(None, &e.to_string()); + unsafe { *err = error_msg.take() }; + raw::REDISMODULE_ERR as i32 + } + } +} + +extern "C" fn enum_configuration_get< + G: EnumConfigurationValue, + T: ConfigurationValue + 'static, +>( + _name: *const c_char, + privdata: *mut c_void, +) -> c_int { + let private_data = unsafe { &*(privdata as *const ConfigrationPrivateData) }; + private_data.get_val().into() +} + +pub fn register_enum_configuration>( + ctx: &Context, + name: &str, + variable: &'static T, + default: G, + flags: ConfigurationFlags, + on_changed: Option>, +) { + let name = CString::new(name).unwrap(); + let (names, vals) = default.get_options(); + assert_eq!(names.len(), vals.len()); + let names: Vec = names + .into_iter() + .map(|v| CString::new(v).unwrap()) + .collect(); + let config_private_data = ConfigrationPrivateData { + variable: variable, + on_changed: on_changed, + phantom: PhantomData::, + }; + unsafe { + raw::RedisModule_RegisterEnumConfig.unwrap()( + ctx.ctx, + name.as_ptr(), + default.into(), + flags.bits() as u32, + names + .iter() + .map(|v| v.as_ptr()) + .collect::>() + .as_mut_ptr(), + vals.as_ptr(), + names.len() as i32, + Some(enum_configuration_get::), + Some(enum_configuration_set::), + None, + Box::into_raw(Box::new(config_private_data)) as *mut c_void, + ); + } +} + +pub fn apply_module_args_as_configuration( + ctx: &Context, + mut args: Vec, +) -> Result<(), RedisError> { + if args.len() == 0 { + return Ok(()); + } + if args.len() % 2 != 0 { + return Err(RedisError::Str( + "Arguments lenght is not devided by 2 (require to be read as module configuration).", + )); + } + args.insert(0, ctx.create_string("set")); + ctx.call( + "config", + args.iter().collect::>().as_slice(), + )?; + Ok(()) +} diff --git a/src/context/info.rs b/src/context/info.rs index 0a6051ae..efd3cdf1 100644 --- a/src/context/info.rs +++ b/src/context/info.rs @@ -1,4 +1,5 @@ use std::ffi::CString; +use std::ptr::NonNull; use crate::Context; use crate::{raw, RedisString}; @@ -23,7 +24,7 @@ impl ServerInfo { if value.is_null() { None } else { - Some(RedisString::new(self.ctx, value)) + Some(RedisString::new(NonNull::new(self.ctx), value)) } } } diff --git a/src/context/keys_cursor.rs b/src/context/keys_cursor.rs index b57532df..aaa3f748 100644 --- a/src/context/keys_cursor.rs +++ b/src/context/keys_cursor.rs @@ -3,6 +3,7 @@ use crate::key::RedisKey; use crate::raw; use crate::redismodule::RedisString; use std::ffi::c_void; +use std::ptr::NonNull; pub struct KeysCursor { inner_cursor: *mut raw::RedisModuleScanCursor, @@ -15,7 +16,7 @@ extern "C" fn scan_callback)>( private_data: *mut ::std::os::raw::c_void, ) { let context = Context::new(ctx); - let key_name = RedisString::new(ctx, key_name); + let key_name = RedisString::new(NonNull::new(ctx), key_name); let redis_key = if key.is_null() { None } else { diff --git a/src/context/mod.rs b/src/context/mod.rs index 9de9ab7e..52b7a9c4 100644 --- a/src/context/mod.rs +++ b/src/context/mod.rs @@ -3,6 +3,7 @@ use std::borrow::Borrow; use std::ffi::CString; use std::os::raw::{c_char, c_int, c_long, c_longlong}; use std::ptr; +use std::ptr::NonNull; use crate::key::{RedisKey, RedisKeyWritable}; use crate::raw::{ModuleOptions, Version}; @@ -14,6 +15,7 @@ use crate::{RedisError, RedisResult, RedisString, RedisValue}; use std::ffi::CStr; use self::call_reply::RootCallReply; +use self::thread_safe::RedisLockIndicator; #[cfg(feature = "experimental-api")] mod timer; @@ -412,7 +414,7 @@ impl Context { #[must_use] pub fn create_string(&self, s: &str) -> RedisString { - RedisString::create(self.ctx, s) + RedisString::create(NonNull::new(self.ctx), s) } #[must_use] @@ -564,6 +566,8 @@ impl Context { } } +unsafe impl RedisLockIndicator for Context {} + bitflags! { /// An object represent ACL permissions. /// Used to check ACL permission using `acl_check_key_permission`. diff --git a/src/context/thread_safe.rs b/src/context/thread_safe.rs index 3524b8ae..50836cb7 100644 --- a/src/context/thread_safe.rs +++ b/src/context/thread_safe.rs @@ -6,12 +6,12 @@ use std::ptr; use crate::context::blocked::BlockedClient; use crate::{raw, Context, RedisResult}; -pub struct RedisGILGuardScope<'ctx, 'mutex, T: Default> { - _context: &'ctx Context, +pub struct RedisGILGuardScope<'ctx, 'mutex, T, G: RedisLockIndicator> { + _context: &'ctx G, mutex: &'mutex RedisGILGuard, } -impl<'ctx, 'mutex, T: Default> Deref for RedisGILGuardScope<'ctx, 'mutex, T> { +impl<'ctx, 'mutex, T, G: RedisLockIndicator> Deref for RedisGILGuardScope<'ctx, 'mutex, T, G> { type Target = T; fn deref(&self) -> &Self::Target { @@ -19,28 +19,54 @@ impl<'ctx, 'mutex, T: Default> Deref for RedisGILGuardScope<'ctx, 'mutex, T> { } } -impl<'ctx, 'mutex, T: Default> DerefMut for RedisGILGuardScope<'ctx, 'mutex, T> { +impl<'ctx, 'mutex, T, G: RedisLockIndicator> DerefMut for RedisGILGuardScope<'ctx, 'mutex, T, G> { fn deref_mut(&mut self) -> &mut Self::Target { unsafe { &mut *self.mutex.obj.get() } } } -#[derive(Default)] -pub struct RedisGILGuard { +/// Whenever the user gets a reference to a struct that +/// implements this trait, it can assume that the Redis GIL +/// is held. Any struct that implements this trait can be +/// used to retrieve objects which are GIL protected (see +/// [RedisGILGuard] for more information) +/// +/// Notice that this trait only gives indication that the +/// GIL is locked, unlike [RedisGILGuard] which protect data +/// access and make sure the protected data is only accesses +/// when the GIL is locked. +/// +/// In general this trait should not be implemented by the +/// user, the crate knows when the Redis GIL is held and will +/// make sure to implement this trait correctly on different +/// struct (such as [Context], [ConfigurationContext], [ContextGuard]). +/// User might also decide to implement this trait but he should +/// carefully consider that because it is easy to make mistakes, +/// this is why the trait is marked as unsafe. +pub unsafe trait RedisLockIndicator {} + +/// This struct allows to guard some data and makes sure +/// the data is only access when the Redis GIL is locked. +/// From example, assuming you module want to save some +/// statistics inside some global variable, but without the +/// need to protect this variable with some mutex (because +/// we know this variable is protected by Redis lock). +/// For example, look at examples/threads.rs +pub struct RedisGILGuard { obj: UnsafeCell, } -impl RedisGILGuard { +impl RedisGILGuard { pub fn new(obj: T) -> RedisGILGuard { RedisGILGuard { obj: UnsafeCell::new(obj), } } - pub fn lock<'mutex, 'ctx>( + pub fn lock<'mutex, 'ctx, G: RedisLockIndicator>( &'mutex self, - context: &'ctx Context, - ) -> RedisGILGuardScope<'ctx, 'mutex, T> { + context: &'ctx G, + ) -> RedisGILGuardScope<'ctx, 'mutex, T, G> { RedisGILGuardScope { _context: context, mutex: self, @@ -48,13 +74,21 @@ impl RedisGILGuard { } } -unsafe impl Sync for RedisGILGuard {} -unsafe impl Send for RedisGILGuard {} +impl Default for RedisGILGuard { + fn default() -> Self { + Self::new(T::default()) + } +} + +unsafe impl Sync for RedisGILGuard {} +unsafe impl Send for RedisGILGuard {} pub struct ContextGuard { ctx: Context, } +unsafe impl RedisLockIndicator for ContextGuard {} + impl Drop for ContextGuard { fn drop(&mut self) { unsafe { raw::RedisModule_ThreadSafeContextUnlock.unwrap()(self.ctx.ctx) }; diff --git a/src/key.rs b/src/key.rs index 1fe01fe8..0f84d940 100644 --- a/src/key.rs +++ b/src/key.rs @@ -3,6 +3,7 @@ use std::ops::Deref; use std::ops::DerefMut; use std::os::raw::c_void; use std::ptr; +use std::ptr::NonNull; use std::time::Duration; use libc::size_t; @@ -252,7 +253,7 @@ impl RedisKeyWritable { return None; } - Some(RedisString::new(self.ctx, ptr)) + Some(RedisString::new(NonNull::new(self.ctx), ptr)) } // `list_pop_head` pops and returns the last element of the list. @@ -267,7 +268,7 @@ impl RedisKeyWritable { return None; } - Some(RedisString::new(self.ctx, ptr)) + Some(RedisString::new(NonNull::new(self.ctx), ptr)) } pub fn set_expire(&self, expire: Duration) -> RedisResult { @@ -287,7 +288,7 @@ impl RedisKeyWritable { } pub fn write(&self, val: &str) -> RedisResult { - let val_str = RedisString::create(self.ctx, val); + let val_str = RedisString::create(NonNull::new(self.ctx), val); match raw::string_set(self.key_inner, val_str.inner) { raw::Status::Ok => REDIS_OK, raw::Status::Err => Err(RedisError::Str("Error while setting key")), @@ -459,7 +460,7 @@ where /// use redis_module::{Context, RedisError, RedisResult, RedisString, RedisValue}; /// /// fn call_hash(ctx: &Context, _: Vec) -> RedisResult { - /// let key_name = RedisString::create(ctx.ctx, "config"); + /// let key_name = RedisString::create(None, "config"); /// let fields = &["username", "password", "email"]; /// let hm: HMGetResult<'_, &str, RedisString> = ctx /// .open_key(&key_name) @@ -477,7 +478,7 @@ where /// use redis_module::key::HMGetResult; /// /// fn call_hash(ctx: &Context, _: Vec) -> RedisResult { - /// let key_name = RedisString::create(ctx.ctx, "config"); + /// let key_name = RedisString::create(None, "config"); /// let fields = &["username", "password", "email"]; /// let hm: HMGetResult<'_, &str, RedisString> = ctx /// .open_key(&key_name) diff --git a/src/lib.rs b/src/lib.rs index f4401651..26a925e3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,6 +14,7 @@ pub mod redisraw; pub mod redisvalue; pub mod stream; +pub mod configuration; mod context; pub mod key; pub mod logging; @@ -27,6 +28,8 @@ pub use crate::context::thread_safe::{DetachedFromClient, ThreadSafeContext}; #[cfg(feature = "experimental-api")] pub use crate::raw::NotifyEvent; +pub use crate::configuration::ConfigurationValue; +pub use crate::configuration::EnumConfigurationValue; pub use crate::context::call_reply::{CallReply, InnerCallReply, RootCallReply}; pub use crate::context::keys_cursor::KeysCursor; pub use crate::context::server_events; diff --git a/src/logging.rs b/src/logging.rs index a20fbb7f..7688b83c 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -4,9 +4,6 @@ use std::ffi::CString; use std::ptr; pub(crate) fn log_internal(ctx: *mut raw::RedisModuleCtx, level: LogLevel, message: &str) { - if cfg!(feature = "test") { - return; - } let level = CString::new(level.as_ref()).unwrap(); let fmt = CString::new(message).unwrap(); unsafe { raw::RedisModule_Log.unwrap()(ctx, level.as_ptr(), fmt.as_ptr()) } diff --git a/src/macros.rs b/src/macros.rs index f6dd5ac6..39bebb7d 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -109,6 +109,39 @@ macro_rules! redis_module { $event_handler:expr ]),* $(,)* ])? + $(configurations: [ + $(i64:[$([ + $i64_configuration_name:expr, + $i64_configuration_val:expr, + $i64_default:expr, + $i64_min:expr, + $i64_max:expr, + $i64_flags_options:expr, + $i64_on_changed:expr + ]),* $(,)*],)? + $(string:[$([ + $string_configuration_name:expr, + $string_configuration_val:expr, + $string_default:expr, + $string_flags_options:expr, + $string_on_changed:expr + ]),* $(,)*],)? + $(bool:[$([ + $bool_configuration_name:expr, + $bool_configuration_val:expr, + $bool_default:expr, + $bool_flags_options:expr, + $bool_on_changed:expr + ]),* $(,)*],)? + $(enum:[$([ + $enum_configuration_name:expr, + $enum_configuration_val:expr, + $enum_default:expr, + $enum_flags_options:expr, + $enum_on_changed:expr + ]),* $(,)*],)? + $(module_args_as_configuration:$use_module_args:expr,)? + ])? ) => { extern "C" fn __info_func( ctx: *mut $crate::raw::RedisModuleInfoCtx, @@ -133,6 +166,11 @@ macro_rules! redis_module { use $crate::raw; use $crate::RedisString; use $crate::server_events::register_server_events; + use $crate::configuration::register_i64_configuration; + use $crate::configuration::register_string_configuration; + use $crate::configuration::register_bool_configuration; + use $crate::configuration::register_enum_configuration; + use $crate::configuration::apply_module_args_as_configuration; // We use a statically sized buffer to avoid allocating. // This is needed since we use a custom allocator that relies on the Redis allocator, @@ -180,6 +218,38 @@ macro_rules! redis_module { )* )? + $( + $( + $( + register_i64_configuration(&context, $i64_configuration_name, $i64_configuration_val, $i64_default, $i64_min, $i64_max, $i64_flags_options, $i64_on_changed); + )* + )? + $( + $( + register_string_configuration(&context, $string_configuration_name, $string_configuration_val, $string_default, $string_flags_options, $string_on_changed); + )* + )? + $( + $( + register_bool_configuration(&context, $bool_configuration_name, $bool_configuration_val, $bool_default, $bool_flags_options, $bool_on_changed); + )* + )? + $( + $( + register_enum_configuration(&context, $enum_configuration_name, $enum_configuration_val, $enum_default, $enum_flags_options, $enum_on_changed); + )* + )? + raw::RedisModule_LoadConfigs.unwrap()(ctx); + $( + if $use_module_args { + if let Err(e) = apply_module_args_as_configuration(&context, args) { + context.log_warning(&e.to_string()); + return raw::Status::Err as c_int; + } + } + )? + )? + raw::register_info_function(ctx, Some(__info_func)); if let Err(e) = register_server_events(&context) { diff --git a/src/raw.rs b/src/raw.rs index 411010c1..5fac4daf 100644 --- a/src/raw.rs +++ b/src/raw.rs @@ -10,6 +10,7 @@ use std::cmp::Ordering; use std::ffi::{CStr, CString}; use std::os::raw::{c_char, c_double, c_int, c_long, c_longlong}; use std::ptr; +use std::ptr::NonNull; use std::slice; use bitflags::bitflags; @@ -501,8 +502,10 @@ pub fn load_string_buffer(rdb: *mut RedisModuleIO) -> Result #[allow(clippy::not_unsafe_ptr_arg_deref)] pub fn replicate(ctx: *mut RedisModuleCtx, command: &str, args: &[&str]) -> Status { - let terminated_args: Vec = - args.iter().map(|s| RedisString::create(ctx, s)).collect(); + let terminated_args: Vec = args + .iter() + .map(|s| RedisString::create(NonNull::new(ctx), s)) + .collect(); let inner_args: Vec<*mut RedisModuleString> = terminated_args.iter().map(|s| s.inner).collect(); @@ -597,7 +600,7 @@ pub fn add_info_section(ctx: *mut RedisModuleInfoCtx, name: Option<&str>) -> Sta #[allow(clippy::not_unsafe_ptr_arg_deref)] pub fn add_info_field_str(ctx: *mut RedisModuleInfoCtx, name: &str, content: &str) -> Status { let name = CString::new(name).unwrap(); - let content = RedisString::create(ptr::null_mut(), content); + let content = RedisString::create(None, content); unsafe { RedisModule_InfoAddFieldString.unwrap()(ctx, name.as_ptr(), content.inner).into() } } diff --git a/src/redismodule.rs b/src/redismodule.rs index 520d9c70..429b0500 100644 --- a/src/redismodule.rs +++ b/src/redismodule.rs @@ -5,6 +5,7 @@ use std::fmt; use std::fmt::Display; use std::ops::Deref; use std::os::raw::{c_char, c_int, c_void}; +use std::ptr::NonNull; use std::slice; use std::str; use std::str::Utf8Error; @@ -106,7 +107,7 @@ pub fn decode_args( ) -> Vec { unsafe { slice::from_raw_parts(argv, argc as usize) } .iter() - .map(|&arg| RedisString::new(ctx, arg)) + .map(|&arg| RedisString::new(NonNull::new(ctx), arg)) .collect() } @@ -125,13 +126,18 @@ impl RedisString { inner } - pub fn new(ctx: *mut raw::RedisModuleCtx, inner: *mut raw::RedisModuleString) -> Self { + pub fn new( + ctx: Option>, + inner: *mut raw::RedisModuleString, + ) -> Self { + let ctx = ctx.map_or(std::ptr::null_mut(), |v| v.as_ptr()); raw::string_retain_string(ctx, inner); Self { ctx, inner } } #[allow(clippy::not_unsafe_ptr_arg_deref)] - pub fn create(ctx: *mut raw::RedisModuleCtx, s: &str) -> Self { + pub fn create(ctx: Option>, s: &str) -> Self { + let ctx = ctx.map_or(std::ptr::null_mut(), |v| v.as_ptr()); let str = CString::new(s).unwrap(); let inner = unsafe { raw::RedisModule_CreateString.unwrap()(ctx, str.as_ptr(), s.len()) }; @@ -288,7 +294,7 @@ impl Clone for RedisString { fn clone(&self) -> Self { let inner = unsafe { raw::RedisModule_CreateStringFromString.unwrap()(self.ctx, self.inner) }; - Self::new(self.ctx, inner) + Self::new(NonNull::new(self.ctx), inner) } } diff --git a/tests/integration.rs b/tests/integration.rs index 54665ae3..0cdd90b4 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -390,3 +390,76 @@ fn test_server_event() -> Result<()> { Ok(()) } + +#[test] +fn test_configuration() -> Result<()> { + let port: u16 = 6495; + let _guards = vec![start_redis_server_with_module("configuration", port) + .with_context(|| "failed to start redis server")?]; + + let config_get = |config: &str| -> Result { + let mut con = + get_redis_connection(port).with_context(|| "failed to connect to redis server")?; + let res: Vec = redis::cmd("config") + .arg(&["get", config]) + .query(&mut con) + .with_context(|| "failed to run flushall")?; + Ok(res[1].clone()) + }; + + let config_set = |config: &str, val: &str| -> Result<()> { + let mut con = + get_redis_connection(port).with_context(|| "failed to connect to redis server")?; + let res: String = redis::cmd("config") + .arg(&["set", config, val]) + .query(&mut con) + .with_context(|| "failed to run flushall")?; + assert_eq!(res, "OK"); + Ok(()) + }; + + assert_eq!(config_get("configuration.i64")?, "10"); + config_set("configuration.i64", "100")?; + assert_eq!(config_get("configuration.i64")?, "100"); + + assert_eq!(config_get("configuration.atomic_i64")?, "10"); + config_set("configuration.atomic_i64", "100")?; + assert_eq!(config_get("configuration.atomic_i64")?, "100"); + + assert_eq!(config_get("configuration.redis_string")?, "default"); + config_set("configuration.redis_string", "new")?; + assert_eq!(config_get("configuration.redis_string")?, "new"); + + assert_eq!(config_get("configuration.string")?, "default"); + config_set("configuration.string", "new")?; + assert_eq!(config_get("configuration.string")?, "new"); + + assert_eq!(config_get("configuration.mutex_string")?, "default"); + config_set("configuration.mutex_string", "new")?; + assert_eq!(config_get("configuration.mutex_string")?, "new"); + + assert_eq!(config_get("configuration.atomic_bool")?, "yes"); + config_set("configuration.atomic_bool", "no")?; + assert_eq!(config_get("configuration.atomic_bool")?, "no"); + + assert_eq!(config_get("configuration.bool")?, "yes"); + config_set("configuration.bool", "no")?; + assert_eq!(config_get("configuration.bool")?, "no"); + + assert_eq!(config_get("configuration.enum")?, "Val1"); + config_set("configuration.enum", "Val2")?; + assert_eq!(config_get("configuration.enum")?, "Val2"); + + assert_eq!(config_get("configuration.enum_mutex")?, "Val1"); + config_set("configuration.enum_mutex", "Val2")?; + assert_eq!(config_get("configuration.enum_mutex")?, "Val2"); + + let mut con = + get_redis_connection(port).with_context(|| "failed to connect to redis server")?; + let res: i64 = redis::cmd("configuration.num_changes") + .query(&mut con) + .with_context(|| "failed to run flushall")?; + assert_eq!(res, 18); // the first configuration initialisation is counted as well, so we will get 18 changes. + + Ok(()) +}