├── src ├── apierror.rs ├── redisraw │ └── mod.rs ├── redismodule.c ├── valkeymodule.c ├── utils.rs ├── context │ ├── auth.rs │ ├── info.rs │ ├── keys_cursor.rs │ ├── timer.rs │ ├── filter.rs │ ├── client.rs │ ├── thread_safe.rs │ └── blocked.rs ├── rediserror.rs ├── native_types.rs ├── error.rs ├── digest.rs ├── stream.rs ├── defrag.rs ├── alloc.rs ├── lib.rs └── logging.rs ├── .gitmodules ├── test.sh ├── .github ├── dependabot.yml ├── release-drafter-config.yml └── workflows │ ├── release-drafter.yml │ ├── freebsd.yml │ └── cratesio-publish.yml ├── .gitignore ├── .dockerignore ├── sbin ├── setup └── system-setup.py ├── examples ├── info_handler_macro.rs ├── ctx_flags.rs ├── info_handler_builder.rs ├── preload.rs ├── hello.rs ├── info.rs ├── scan_keys.rs ├── block.rs ├── expire.rs ├── info_handler_struct.rs ├── keys_pos.rs ├── info_handler_multiple_sections.rs ├── load_unload.rs ├── stream.rs ├── string.rs ├── lists.rs ├── timer.rs ├── acl.rs ├── open_key_with_flags.rs ├── test_helper.rs ├── response.rs ├── threads.rs ├── events.rs ├── data_type2.rs ├── filter2.rs ├── filter1.rs ├── subcmd.rs ├── crontab.rs ├── proc_macro_commands.rs ├── data_type.rs ├── client.rs ├── call.rs └── configuration.rs ├── valkeymodule-rs-macros-internals ├── Cargo.toml └── src │ ├── api_versions.rs │ └── lib.rs ├── valkeymodule-rs-macros ├── Cargo.toml └── src │ ├── info_section.rs │ └── valkey_value.rs ├── Dockerfile ├── LICENSE ├── Makefile ├── unused └── hello_no_macros.rs ├── README.md ├── Cargo.toml ├── tests └── utils.rs └── .circleci └── config.yml /src/apierror.rs: -------------------------------------------------------------------------------- 1 | pub type APIError = String; 2 | pub type APIResult = Result; 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "deps/readies"] 2 | path = deps/readies 3 | url = https://github.com/redislabsmodules/readies 4 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # TODO cargo test --all --all-targets --no-default-features 3 | cargo test --all --no-default-features 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | ignore: 9 | - dependency-name: bindgen 10 | versions: 11 | - 0.56.0 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | **/target/ 4 | 5 | # These are backup files generated by rustfmt 6 | **/*.rs.bk 7 | 8 | # IntelliJ 9 | .idea 10 | src/redisraw/bindings.rs 11 | 12 | # VS Code 13 | .vscode 14 | 15 | # Valkey database 16 | *.rdb 17 | 18 | # Debugger-related files: 19 | .gdb_history 20 | 21 | venv/ 22 | tmp/* 23 | shutdown_log.txt 24 | -------------------------------------------------------------------------------- /src/redisraw/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_upper_case_globals)] 2 | #![allow(non_camel_case_types)] 3 | #![allow(non_snake_case)] 4 | #![allow(dead_code)] 5 | 6 | // Workaround for https://github.com/rust-lang/rust-bindgen/issues/1651#issuecomment-848479168 7 | #[allow(deref_nullptr)] 8 | pub mod bindings { 9 | include!(concat!(env!("OUT_DIR"), "/bindings.rs")); 10 | } 11 | 12 | // See: https://users.rust-lang.org/t/bindgen-generate-options-and-some-are-none/14027 13 | -------------------------------------------------------------------------------- /src/redismodule.c: -------------------------------------------------------------------------------- 1 | #include "redismodule.h" 2 | 3 | // RedisModule_Init is defined as a static function and so won't be exported as 4 | // a symbol. Export a version under a slightly different name so that we can 5 | // get access to it from Rust. 6 | 7 | int Export_RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int apiver) { 8 | return RedisModule_Init(ctx, name, ver, apiver); 9 | } 10 | 11 | void Export_RedisModule_InitAPI(RedisModuleCtx *ctx) { 12 | RedisModule_InitAPI(ctx); 13 | } 14 | -------------------------------------------------------------------------------- /src/valkeymodule.c: -------------------------------------------------------------------------------- 1 | #include "valkeymodule.h" 2 | 3 | // ValkeyModule_Init is defined as a static function and so won't be exported as 4 | // a symbol. Export a version under a slightly different name so that we can 5 | // get access to it from Rust. 6 | 7 | int Export_ValkeyModule_Init(ValkeyModuleCtx *ctx, const char *name, int ver, int apiver) { 8 | return ValkeyModule_Init(ctx, name, ver, apiver); 9 | } 10 | 11 | void Export_ValkeyModule_InitAPI(ValkeyModuleCtx *ctx) { 12 | ValkeyModule_InitAPI(ctx); 13 | } 14 | -------------------------------------------------------------------------------- /.github/release-drafter-config.yml: -------------------------------------------------------------------------------- 1 | name-template: 'Version $NEXT_PATCH_VERSION' 2 | tag-template: 'v$NEXT_PATCH_VERSION' 3 | categories: 4 | - title: 'Features' 5 | labels: 6 | - 'feature' 7 | - 'enhancement' 8 | - title: 'Bug Fixes' 9 | labels: 10 | - 'fix' 11 | - 'bugfix' 12 | - 'bug' 13 | - title: 'Maintenance' 14 | label: 'chore' 15 | change-template: '- $TITLE (#$NUMBER)' 16 | exclude-labels: 17 | - 'skip-changelog' 18 | template: | 19 | ## Changes 20 | 21 | $CHANGES 22 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | /bin/ 2 | /target/ 3 | 4 | # Object files 5 | *.o 6 | *.ko 7 | *.obj 8 | *.elf 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Libraries 15 | *.lib 16 | *.a 17 | *.la 18 | *.lo 19 | 20 | # Shared objects (inc. Windows DLLs) 21 | *.dll 22 | *.so 23 | *.so.* 24 | *.dylib 25 | 26 | # Executables 27 | *.exe 28 | *.out 29 | *.app 30 | *.i*86 31 | *.x86_64 32 | *.hex 33 | 34 | # Debug files 35 | *.dSYM/ 36 | *.su 37 | 38 | # Python 39 | .env/ 40 | *.pyc 41 | 42 | # mkdocs 43 | /site/ 44 | 45 | .vscode/ 46 | -------------------------------------------------------------------------------- /sbin/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PROGNAME="${BASH_SOURCE[0]}" 4 | HERE="$(cd "$(dirname "$PROGNAME")" &>/dev/null && pwd)" 5 | ROOT=$(cd $HERE/.. && pwd) 6 | READIES=$ROOT/deps/readies 7 | . $READIES/shibumi/defs 8 | 9 | export HOMEBREW_NO_AUTO_UPDATE=1 10 | 11 | $READIES/bin/getpy3 12 | pyenv="$(get_profile_d)/pyenv.sh" 13 | if [[ -f $pyenv ]]; then 14 | . $pyenv 15 | fi 16 | if [[ $VERBOSE == 1 ]]; then 17 | python3 --version 18 | python3 -m pip list 19 | fi 20 | 21 | $ROOT/sbin/system-setup.py 22 | if [[ $VERBOSE == 1 ]]; then 23 | python3 -m pip list 24 | fi 25 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use regex::Regex; 2 | 3 | /// Extracts regexp captures 4 | /// 5 | /// Extract from `s` the captures defined in `reg_exp` 6 | pub fn get_regexp_captures<'a>(s: &'a str, reg_exp: &str) -> Option> { 7 | Regex::new(reg_exp).map_or_else( 8 | |_| None, 9 | |re| { 10 | let mut res: Vec<&str> = Vec::new(); 11 | re.captures_iter(s).for_each(|captures| { 12 | for i in 1..captures.len() { 13 | res.push(captures.get(i).map_or_else(|| "", |m| m.as_str())); 14 | } 15 | }); 16 | Some(res) 17 | }, 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | # branches to consider in the event; optional, defaults to all 6 | branches: 7 | - master 8 | 9 | jobs: 10 | update_release_draft: 11 | runs-on: ubuntu-latest 12 | steps: 13 | # Drafts your next Release notes as Pull Requests are merged into "master" 14 | - uses: release-drafter/release-drafter@v5 15 | with: 16 | # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml 17 | config-name: release-drafter-config.yml 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | 21 | -------------------------------------------------------------------------------- /examples/info_handler_macro.rs: -------------------------------------------------------------------------------- 1 | use valkey_module::alloc::ValkeyAlloc; 2 | use valkey_module::InfoContext; 3 | use valkey_module::{valkey_module, ValkeyResult}; 4 | use valkey_module_macros::info_command_handler; 5 | 6 | #[info_command_handler] 7 | fn add_info(ctx: &InfoContext, _for_crash_report: bool) -> ValkeyResult<()> { 8 | ctx.builder() 9 | .add_section("info") 10 | .field("field", "value")? 11 | .build_section()? 12 | .build_info() 13 | .map(|_| ()) 14 | } 15 | 16 | ////////////////////////////////////////////////////// 17 | 18 | valkey_module! { 19 | name: "info_handler_macro", 20 | version: 1, 21 | allocator: (ValkeyAlloc, ValkeyAlloc), 22 | data_types: [], 23 | commands: [], 24 | } 25 | -------------------------------------------------------------------------------- /examples/ctx_flags.rs: -------------------------------------------------------------------------------- 1 | use valkey_module::alloc::ValkeyAlloc; 2 | use valkey_module::{ 3 | valkey_module, Context, ContextFlags, ValkeyResult, ValkeyString, ValkeyValue, 4 | }; 5 | 6 | fn role(ctx: &Context, _args: Vec) -> ValkeyResult { 7 | Ok(ValkeyValue::SimpleStringStatic( 8 | if ctx.get_flags().contains(ContextFlags::MASTER) { 9 | "master" 10 | } else { 11 | "slave" 12 | }, 13 | )) 14 | } 15 | 16 | ////////////////////////////////////////////////////// 17 | 18 | valkey_module! { 19 | name: "ctx_flags", 20 | version: 1, 21 | allocator: (ValkeyAlloc, ValkeyAlloc), 22 | data_types: [], 23 | commands: [ 24 | ["my_role", role, "readonly", 0, 0, 0], 25 | ], 26 | } 27 | -------------------------------------------------------------------------------- /valkeymodule-rs-macros-internals/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "valkey-module-macros-internals" 3 | version = "0.1.10" 4 | authors = ["Dmitry Polyakovsky "] 5 | edition = "2021" 6 | description = "A macros crate for valkeymodule-rs" 7 | license = "BSD-3-Clause" 8 | repository = "https://github.com/valkey-io/valkeymodule-rs" 9 | keywords = ["valkey", "plugin"] 10 | categories = ["database", "api-bindings"] 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [dependencies] 15 | syn = { version="1.0", features = ["full"]} 16 | quote = "1.0" 17 | proc-macro2 = "1" 18 | serde = { version = "1", features = ["derive"] } 19 | serde_syn = "0.1.0" 20 | 21 | [lib] 22 | name = "valkey_module_macros" 23 | path = "src/lib.rs" 24 | proc-macro = true 25 | -------------------------------------------------------------------------------- /examples/info_handler_builder.rs: -------------------------------------------------------------------------------- 1 | use valkey_module::alloc::ValkeyAlloc; 2 | use valkey_module::InfoContext; 3 | use valkey_module::{valkey_module, ValkeyResult}; 4 | use valkey_module_macros::info_command_handler; 5 | 6 | #[info_command_handler] 7 | fn add_info(ctx: &InfoContext, _for_crash_report: bool) -> ValkeyResult<()> { 8 | ctx.builder() 9 | .add_section("info") 10 | .field("field", "value")? 11 | .add_dictionary("dictionary") 12 | .field("key", "value")? 13 | .build_dictionary()? 14 | .build_section()? 15 | .build_info()?; 16 | 17 | Ok(()) 18 | } 19 | 20 | ////////////////////////////////////////////////////// 21 | 22 | valkey_module! { 23 | name: "info_handler_builder", 24 | version: 1, 25 | allocator: (ValkeyAlloc, ValkeyAlloc), 26 | data_types: [], 27 | commands: [], 28 | } 29 | -------------------------------------------------------------------------------- /examples/preload.rs: -------------------------------------------------------------------------------- 1 | use valkey_module::alloc::ValkeyAlloc; 2 | use valkey_module::{valkey_module, Context, Status, ValkeyString}; 3 | 4 | fn preload(ctx: &Context, args: &[ValkeyString]) -> Status { 5 | // perform preload validations here, useful for MODULE LOAD 6 | // unlike init which is called at the end of the valkey_module! macro this is called at the beginning 7 | let version = ctx.get_server_version().unwrap(); 8 | ctx.log_notice(&format!( 9 | "preload for server version {:?} with args: {:?}", 10 | version, args 11 | )); 12 | // respond with either Status::Ok or Status::Err (if you want to prevent module loading) 13 | Status::Ok 14 | } 15 | 16 | valkey_module! { 17 | name: "preload", 18 | version: 1, 19 | allocator: (ValkeyAlloc, ValkeyAlloc), 20 | data_types: [], 21 | preload: preload, 22 | commands: [], 23 | } 24 | -------------------------------------------------------------------------------- /examples/hello.rs: -------------------------------------------------------------------------------- 1 | use valkey_module::alloc::ValkeyAlloc; 2 | use valkey_module::{valkey_module, Context, ValkeyError, ValkeyResult, ValkeyString}; 3 | 4 | fn hello_mul(_: &Context, args: Vec) -> ValkeyResult { 5 | if args.len() < 2 { 6 | return Err(ValkeyError::WrongArity); 7 | } 8 | 9 | let nums = args 10 | .into_iter() 11 | .skip(1) 12 | .map(|s| s.parse_integer()) 13 | .collect::, ValkeyError>>()?; 14 | 15 | let product = nums.iter().product(); 16 | let mut response = nums; 17 | response.push(product); 18 | 19 | Ok(response.into()) 20 | } 21 | 22 | ////////////////////////////////////////////////////// 23 | 24 | valkey_module! { 25 | name: "hello", 26 | version: 1, 27 | allocator: (ValkeyAlloc, ValkeyAlloc), 28 | data_types: [], 29 | commands: [ 30 | ["hello.mul", hello_mul, "", 0, 0, 0], 31 | ], 32 | } 33 | -------------------------------------------------------------------------------- /examples/info.rs: -------------------------------------------------------------------------------- 1 | use valkey_module::alloc::ValkeyAlloc; 2 | use valkey_module::{ 3 | valkey_module, Context, NextArg, ValkeyError, ValkeyResult, ValkeyString, ValkeyValue, 4 | }; 5 | 6 | fn info_cmd(ctx: &Context, args: Vec) -> ValkeyResult { 7 | if args.len() < 3 { 8 | return Err(ValkeyError::WrongArity); 9 | } 10 | 11 | let mut args = args.into_iter().skip(1); 12 | 13 | let section = args.next_str()?; 14 | let field = args.next_str()?; 15 | 16 | let server_info = ctx.server_info(section); 17 | Ok(server_info 18 | .field(field) 19 | .map_or(ValkeyValue::Null, ValkeyValue::BulkValkeyString)) 20 | } 21 | 22 | ////////////////////////////////////////////////////// 23 | 24 | valkey_module! { 25 | name: "info", 26 | version: 1, 27 | allocator: (ValkeyAlloc, ValkeyAlloc), 28 | data_types: [], 29 | commands: [ 30 | ["infoex", info_cmd, "", 0, 0, 0], 31 | ], 32 | } 33 | -------------------------------------------------------------------------------- /examples/scan_keys.rs: -------------------------------------------------------------------------------- 1 | use valkey_module::alloc::ValkeyAlloc; 2 | use valkey_module::{ 3 | key::ValkeyKey, valkey_module, Context, KeysCursor, ValkeyResult, ValkeyString, ValkeyValue, 4 | }; 5 | 6 | fn scan_keys(ctx: &Context, _args: Vec) -> ValkeyResult { 7 | let cursor = KeysCursor::new(); 8 | let mut res = Vec::new(); 9 | 10 | let scan_callback = |_ctx: &Context, key_name: ValkeyString, _key: Option<&ValkeyKey>| { 11 | res.push(ValkeyValue::BulkValkeyString(key_name)); 12 | }; 13 | 14 | while cursor.scan(ctx, &scan_callback) { 15 | // do nothing 16 | } 17 | Ok(ValkeyValue::Array(res)) 18 | } 19 | 20 | ////////////////////////////////////////////////////// 21 | 22 | valkey_module! { 23 | name: "scan", 24 | version: 1, 25 | allocator: (ValkeyAlloc, ValkeyAlloc), 26 | data_types: [], 27 | commands: [ 28 | ["scan_keys", scan_keys, "readonly", 0, 0, 0], 29 | ], 30 | } 31 | -------------------------------------------------------------------------------- /examples/block.rs: -------------------------------------------------------------------------------- 1 | use std::thread; 2 | use std::time::Duration; 3 | use valkey_module::alloc::ValkeyAlloc; 4 | use valkey_module::{ 5 | valkey_module, Context, ThreadSafeContext, ValkeyResult, ValkeyString, ValkeyValue, 6 | }; 7 | 8 | fn block(ctx: &Context, _args: Vec) -> ValkeyResult { 9 | let blocked_client = ctx.block_client(); 10 | 11 | thread::spawn(move || { 12 | let thread_ctx = ThreadSafeContext::with_blocked_client(blocked_client); 13 | thread::sleep(Duration::from_millis(1000)); 14 | thread_ctx.reply(Ok("42".into())); 15 | }); 16 | 17 | // We will reply later, from the thread 18 | Ok(ValkeyValue::NoReply) 19 | } 20 | 21 | ////////////////////////////////////////////////////// 22 | 23 | valkey_module! { 24 | name: "block", 25 | version: 1, 26 | allocator: (ValkeyAlloc, ValkeyAlloc), 27 | data_types: [], 28 | commands: [ 29 | ["block", block, "", 0, 0, 0], 30 | ], 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/freebsd.yml: -------------------------------------------------------------------------------- 1 | name: freebsd 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: macos-12 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: build 17 | uses: vmactions/freebsd-vm@v0.3.0 18 | with: 19 | usesh: true 20 | sync: rsync 21 | copyback: false 22 | prepare: pkg install -y bash curl lang/rust devel/llvm-devel 23 | run: | 24 | cargo build --all --all-targets --verbose 25 | cargo install cargo-deny 26 | cargo deny check licenses 27 | cargo deny check bans 28 | curl https://sh.rustup.rs -sSf > rustup.sh 29 | sh rustup.sh -y 30 | . $HOME/.cargo/env 31 | rustup target add i686-unknown-freebsd 32 | cargo build --all --all-targets --verbose --no-default-features --target i686-unknown-freebsd 33 | 34 | -------------------------------------------------------------------------------- /examples/expire.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | use valkey_module::alloc::ValkeyAlloc; 3 | use valkey_module::{valkey_module, Context, NextArg, ValkeyError, ValkeyResult, ValkeyString}; 4 | 5 | fn expire_cmd(ctx: &Context, args: Vec) -> ValkeyResult { 6 | if args.len() < 3 { 7 | return Err(ValkeyError::WrongArity); 8 | } 9 | 10 | let mut args = args.into_iter().skip(1); 11 | let key_name = args.next_arg()?; 12 | let ttl_sec = args.next_i64()?; 13 | let key = ctx.open_key_writable(&key_name); 14 | if ttl_sec >= 0 { 15 | key.set_expire(Duration::new(ttl_sec as u64, 0)) 16 | } else { 17 | key.remove_expire() 18 | } 19 | } 20 | 21 | ////////////////////////////////////////////////////// 22 | 23 | valkey_module! { 24 | name: "expire", 25 | version: 1, 26 | allocator: (ValkeyAlloc, ValkeyAlloc), 27 | data_types: [], 28 | commands: [ 29 | ["expire.cmd", expire_cmd, "write fast deny-oom", 1, 1, 1], 30 | ], 31 | } 32 | -------------------------------------------------------------------------------- /examples/info_handler_struct.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use valkey_module::alloc::ValkeyAlloc; 4 | use valkey_module::InfoContext; 5 | use valkey_module::{valkey_module, ValkeyResult}; 6 | use valkey_module_macros::{info_command_handler, InfoSection}; 7 | 8 | #[derive(Debug, Clone, InfoSection)] 9 | struct Info { 10 | field: String, 11 | dictionary: HashMap, 12 | } 13 | 14 | #[info_command_handler] 15 | fn add_info(ctx: &InfoContext, _for_crash_report: bool) -> ValkeyResult<()> { 16 | let mut dictionary = HashMap::new(); 17 | dictionary.insert("key".to_owned(), "value".into()); 18 | let data = Info { 19 | field: "value".to_owned(), 20 | dictionary, 21 | }; 22 | ctx.build_one_section(data) 23 | } 24 | 25 | ////////////////////////////////////////////////////// 26 | 27 | valkey_module! { 28 | name: "info_handler_struct", 29 | version: 1, 30 | allocator: (ValkeyAlloc, ValkeyAlloc), 31 | data_types: [], 32 | commands: [], 33 | } 34 | -------------------------------------------------------------------------------- /src/context/auth.rs: -------------------------------------------------------------------------------- 1 | use crate::{raw, Context, ValkeyString}; 2 | use std::os::raw::{c_char, c_int}; 3 | use std::ptr; 4 | 5 | impl Context { 6 | /// Authenticates a client using an ACL user 7 | /// 8 | /// # Arguments 9 | /// * `username` - ACL username to authenticate with 10 | /// 11 | /// # Returns 12 | /// * `Status::Ok` - Authentication successful 13 | /// * `Status::Err` - Authentication failed 14 | pub fn authenticate_client_with_acl_user(&self, username: &ValkeyString) -> raw::Status { 15 | let result = unsafe { 16 | raw::RedisModule_AuthenticateClientWithACLUser.unwrap()( 17 | self.ctx, 18 | username.as_ptr().cast::(), 19 | username.len(), 20 | None, 21 | ptr::null_mut(), 22 | ptr::null_mut(), 23 | ) 24 | }; 25 | 26 | if result == raw::REDISMODULE_OK as c_int { 27 | raw::Status::Ok 28 | } else { 29 | raw::Status::Err 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/keys_pos.rs: -------------------------------------------------------------------------------- 1 | use valkey_module::alloc::ValkeyAlloc; 2 | use valkey_module::{valkey_module, Context, ValkeyError, ValkeyResult, ValkeyString, ValkeyValue}; 3 | 4 | fn keys_pos(ctx: &Context, args: Vec) -> ValkeyResult { 5 | // Number of args (excluding command name) must be even 6 | if (args.len() - 1) % 2 != 0 { 7 | return Err(ValkeyError::WrongArity); 8 | } 9 | 10 | if ctx.is_keys_position_request() { 11 | for i in 1..args.len() { 12 | if (i - 1) % 2 == 0 { 13 | ctx.key_at_pos(i as i32); 14 | } 15 | } 16 | return Ok(ValkeyValue::NoReply); 17 | } 18 | 19 | let reply: Vec<_> = args.iter().skip(1).step_by(2).collect(); 20 | 21 | Ok(reply.into()) 22 | } 23 | 24 | ////////////////////////////////////////////////////// 25 | 26 | valkey_module! { 27 | name: "keys_pos", 28 | version: 1, 29 | allocator: (ValkeyAlloc, ValkeyAlloc), 30 | data_types: [], 31 | commands: [ 32 | ["keys_pos", keys_pos, "getkeys-api", 1, 1, 1], 33 | ], 34 | } 35 | -------------------------------------------------------------------------------- /examples/info_handler_multiple_sections.rs: -------------------------------------------------------------------------------- 1 | use valkey_module::alloc::ValkeyAlloc; 2 | use valkey_module::InfoContext; 3 | use valkey_module::{valkey_module, ValkeyResult}; 4 | use valkey_module_macros::{info_command_handler, InfoSection}; 5 | 6 | #[derive(Debug, Clone, InfoSection)] 7 | struct InfoSection1 { 8 | field_1: String, 9 | } 10 | 11 | #[derive(Debug, Clone, InfoSection)] 12 | struct InfoSection2 { 13 | field_2: String, 14 | } 15 | 16 | #[info_command_handler] 17 | fn add_info(ctx: &InfoContext, _for_crash_report: bool) -> ValkeyResult<()> { 18 | let data = InfoSection1 { 19 | field_1: "value1".to_owned(), 20 | }; 21 | let _ = ctx.build_one_section(data)?; 22 | 23 | let data = InfoSection2 { 24 | field_2: "value2".to_owned(), 25 | }; 26 | 27 | ctx.build_one_section(data) 28 | } 29 | 30 | ////////////////////////////////////////////////////// 31 | 32 | valkey_module! { 33 | name: "info_handler_multiple_sections", 34 | version: 1, 35 | allocator: (ValkeyAlloc, ValkeyAlloc), 36 | data_types: [], 37 | commands: [], 38 | } 39 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # BUILD redisfab/redismodule-rs:${VERSION}-${ARCH}-${OSNICK} 2 | 3 | ARG REDIS_VER=7.2-rc1 4 | 5 | # bullseye|bionic|xenial|centos8|centos7 6 | ARG OSNICK=bullseye 7 | 8 | # ARCH=x64|arm64v8|arm32v7 9 | ARG ARCH=x64 10 | 11 | ARG TEST=0 12 | 13 | #---------------------------------------------------------------------------------------------- 14 | FROM redisfab/redis:${REDIS_VER}-${ARCH}-${OSNICK} AS redis 15 | FROM debian:bullseye-slim AS builder 16 | 17 | ARG OSNICK 18 | ARG OS 19 | ARG ARCH 20 | ARG REDIS_VER 21 | ARG TEST 22 | 23 | RUN if [ -f /root/.profile ]; then sed -ie 's/mesg n/tty -s \&\& mesg -n/g' /root/.profile; fi 24 | SHELL ["/bin/bash", "-l", "-c"] 25 | 26 | RUN echo "Building for ${OSNICK} (${OS}) for ${ARCH} [with Redis ${REDIS_VER}]" 27 | 28 | ADD . /build 29 | WORKDIR /build 30 | 31 | RUN ./sbin/setup 32 | RUN make info 33 | RUN make 34 | 35 | RUN set -ex ;\ 36 | if [ "$TEST" = "1" ]; then TEST= make test; fi 37 | 38 | #---------------------------------------------------------------------------------------------- 39 | FROM redisfab/redis:${REDIS_VER}-${ARCH}-${OSNICK} 40 | 41 | ARG REDIS_VER 42 | 43 | ENV LIBDIR /usr/lib/redis/modules 44 | WORKDIR /data 45 | RUN mkdir -p "$LIBDIR" 46 | 47 | COPY --from=builder /build/bin/artifacts/ /var/opt/redislabs/artifacts 48 | 49 | EXPOSE 6379 50 | CMD ["redis-server"] 51 | -------------------------------------------------------------------------------- /examples/load_unload.rs: -------------------------------------------------------------------------------- 1 | use valkey_module::alloc::ValkeyAlloc; 2 | use valkey_module::{logging::ValkeyLogLevel, valkey_module, Context, Status, ValkeyString}; 3 | 4 | static mut GLOBAL_STATE: Option = None; 5 | 6 | fn init(ctx: &Context, args: &[ValkeyString]) -> Status { 7 | let (before, after) = unsafe { 8 | let before = GLOBAL_STATE.clone(); 9 | GLOBAL_STATE.replace(format!("Args passed: {}", args.join(", "))); 10 | let after = GLOBAL_STATE.clone(); 11 | (before, after) 12 | }; 13 | ctx.log( 14 | ValkeyLogLevel::Warning, 15 | &format!("Update global state on LOAD. BEFORE: {before:?}, AFTER: {after:?}",), 16 | ); 17 | 18 | Status::Ok 19 | } 20 | 21 | fn deinit(ctx: &Context) -> Status { 22 | let (before, after) = unsafe { 23 | let before = GLOBAL_STATE.take(); 24 | let after = GLOBAL_STATE.clone(); 25 | (before, after) 26 | }; 27 | ctx.log( 28 | ValkeyLogLevel::Warning, 29 | &format!("Update global state on UNLOAD. BEFORE: {before:?}, AFTER: {after:?}"), 30 | ); 31 | 32 | Status::Ok 33 | } 34 | 35 | ////////////////////////////////////////////////////// 36 | 37 | valkey_module! { 38 | name: "load_unload", 39 | version: 1, 40 | allocator: (ValkeyAlloc, ValkeyAlloc), 41 | data_types: [], 42 | init: init, 43 | deinit: deinit, 44 | commands: [], 45 | } 46 | -------------------------------------------------------------------------------- /src/context/info.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::CString; 2 | use std::ptr::NonNull; 3 | 4 | use crate::Context; 5 | use crate::{raw, ValkeyString}; 6 | 7 | pub struct ServerInfo { 8 | ctx: *mut raw::RedisModuleCtx, 9 | pub(crate) inner: *mut raw::RedisModuleServerInfoData, 10 | } 11 | 12 | impl Drop for ServerInfo { 13 | fn drop(&mut self) { 14 | unsafe { raw::RedisModule_FreeServerInfo.unwrap()(self.ctx, self.inner) }; 15 | } 16 | } 17 | 18 | impl ServerInfo { 19 | pub fn field(&self, field: &str) -> Option { 20 | let field = CString::new(field).unwrap(); 21 | let value = unsafe { 22 | raw::RedisModule_ServerInfoGetField.unwrap()(self.ctx, self.inner, field.as_ptr()) 23 | }; 24 | if value.is_null() { 25 | None 26 | } else { 27 | Some(ValkeyString::new(NonNull::new(self.ctx), value)) 28 | } 29 | } 30 | } 31 | 32 | impl Context { 33 | #[must_use] 34 | pub fn server_info(&self, section: &str) -> ServerInfo { 35 | let section = CString::new(section).unwrap(); 36 | let server_info = unsafe { 37 | raw::RedisModule_GetServerInfo.unwrap()( 38 | self.ctx, // ctx 39 | section.as_ptr(), // section 40 | ) 41 | }; 42 | 43 | ServerInfo { 44 | ctx: self.ctx, 45 | inner: server_info, 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /examples/stream.rs: -------------------------------------------------------------------------------- 1 | use valkey_module::alloc::ValkeyAlloc; 2 | use valkey_module::raw::{KeyType, RedisModuleStreamID}; 3 | use valkey_module::{ 4 | valkey_module, Context, NextArg, ValkeyError, ValkeyResult, ValkeyString, ValkeyValue, 5 | }; 6 | 7 | fn stream_read_from(ctx: &Context, args: Vec) -> ValkeyResult { 8 | let mut args = args.into_iter().skip(1); 9 | 10 | let stream_key = args.next_arg()?; 11 | 12 | let stream = ctx.open_key(&stream_key); 13 | let key_type = stream.key_type(); 14 | 15 | if key_type != KeyType::Stream { 16 | return Err(ValkeyError::WrongType); 17 | } 18 | 19 | let mut iter = stream.get_stream_iterator(false)?; 20 | let element = iter.next(); 21 | let id_to_keep = iter.next().as_ref().map_or_else( 22 | || RedisModuleStreamID { 23 | ms: u64::MAX, 24 | seq: u64::MAX, 25 | }, 26 | |e| e.id, 27 | ); 28 | 29 | let stream = ctx.open_key_writable(&stream_key); 30 | stream.trim_stream_by_id(id_to_keep, false)?; 31 | Ok(match element { 32 | Some(e) => ValkeyValue::BulkString(format!("{}-{}", e.id.ms, e.id.seq)), 33 | None => ValkeyValue::Null, 34 | }) 35 | } 36 | 37 | ////////////////////////////////////////////////////// 38 | 39 | valkey_module! { 40 | name: "stream", 41 | version: 1, 42 | allocator: (ValkeyAlloc, ValkeyAlloc), 43 | data_types: [], 44 | commands: [ 45 | ["STREAM_POP", stream_read_from, "write", 1, 1, 1], 46 | ], 47 | } 48 | -------------------------------------------------------------------------------- /examples/string.rs: -------------------------------------------------------------------------------- 1 | use valkey_module::alloc::ValkeyAlloc; 2 | use valkey_module::{ 3 | valkey_module, Context, NextArg, ValkeyError, ValkeyResult, ValkeyString, ValkeyValue, 4 | }; 5 | 6 | fn string_set(ctx: &Context, args: Vec) -> ValkeyResult { 7 | if args.len() < 3 { 8 | return Err(ValkeyError::WrongArity); 9 | } 10 | 11 | let mut args = args.into_iter().skip(1); 12 | let key_name = args.next_arg()?; 13 | let value = args.next_arg()?; 14 | 15 | let key = ctx.open_key_writable(&key_name); 16 | let mut dma = key.as_string_dma()?; 17 | dma.write(value.as_slice()) 18 | .map(|_| ValkeyValue::SimpleStringStatic("OK")) 19 | } 20 | 21 | fn string_get(ctx: &Context, args: Vec) -> ValkeyResult { 22 | if args.len() < 2 { 23 | return Err(ValkeyError::WrongArity); 24 | } 25 | 26 | let mut args = args.into_iter().skip(1); 27 | let key_name = args.next_arg()?; 28 | 29 | let key = ctx.open_key(&key_name); 30 | let res = key.read()?.map_or(ValkeyValue::Null, |v| { 31 | ValkeyValue::StringBuffer(Vec::from(v)) 32 | }); 33 | Ok(res) 34 | } 35 | 36 | ////////////////////////////////////////////////////// 37 | 38 | valkey_module! { 39 | name: "string", 40 | version: 1, 41 | allocator: (ValkeyAlloc, ValkeyAlloc), 42 | data_types: [], 43 | commands: [ 44 | ["string.set", string_set, "write fast deny-oom", 1, 1, 1], 45 | ["string.get", string_get, "readonly", 1, 1, 1], 46 | ], 47 | } 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, Redis Labs Modules 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /examples/lists.rs: -------------------------------------------------------------------------------- 1 | use valkey_module::alloc::ValkeyAlloc; 2 | use valkey_module::raw::KeyType; 3 | use valkey_module::{ 4 | valkey_module, Context, NextArg, ValkeyError, ValkeyResult, ValkeyString, ValkeyValue, 5 | }; 6 | 7 | // LPOPRPUSH source destination 8 | // Pops and returns the first element (head) of the list stored at 'source' 9 | // and pushes the element to the last position (tail) of the list stored at 10 | // 'destination'. 11 | fn lpoprpush(ctx: &Context, args: Vec) -> ValkeyResult { 12 | let mut args = args.into_iter().skip(1); 13 | 14 | let src = args.next_arg()?; 15 | let dst = args.next_arg()?; 16 | 17 | let src_key = ctx.open_key_writable(&src); 18 | let dst_key = ctx.open_key_writable(&dst); 19 | 20 | let src_type = src_key.key_type(); 21 | let dst_type = dst_key.key_type(); 22 | 23 | if (src_type != KeyType::Empty && src_type != KeyType::List) 24 | || (dst_type != KeyType::Empty && dst_type != KeyType::List) 25 | { 26 | return Err(ValkeyError::WrongType); 27 | } 28 | 29 | match src_key.list_pop_head() { 30 | None => Ok(ValkeyValue::Null), 31 | Some(value) => { 32 | let ret_cpy = value.clone(); 33 | dst_key.list_push_tail(value); 34 | Ok(ValkeyValue::BulkString(ret_cpy.into())) 35 | } 36 | } 37 | } 38 | 39 | ////////////////////////////////////////////////////// 40 | 41 | valkey_module! { 42 | name: "lists", 43 | version: 1, 44 | allocator: (ValkeyAlloc, ValkeyAlloc), 45 | data_types: [], 46 | commands: [ 47 | ["LPOPRPUSH", lpoprpush, "write fast deny-oom", 1, 2, 1], 48 | ], 49 | } 50 | -------------------------------------------------------------------------------- /examples/timer.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | use valkey_module::alloc::ValkeyAlloc; 3 | use valkey_module::{valkey_module, Context, NextArg, ValkeyResult, ValkeyString}; 4 | 5 | fn callback(ctx: &Context, data: String) { 6 | ctx.log_debug(format!("[callback]: {}", data).as_str()); 7 | } 8 | 9 | type MyData = String; 10 | 11 | fn timer_create(ctx: &Context, args: Vec) -> ValkeyResult { 12 | let mut args = args.into_iter().skip(1); 13 | let duration = args.next_i64()?; 14 | let data: MyData = args.next_string()?; 15 | 16 | let timer_id = ctx.create_timer(Duration::from_millis(duration as u64), callback, data); 17 | 18 | Ok(format!("{}", timer_id).into()) 19 | } 20 | 21 | fn timer_info(ctx: &Context, args: Vec) -> ValkeyResult { 22 | let mut args = args.into_iter().skip(1); 23 | let timer_id = args.next_u64()?; 24 | 25 | let (remaining, data): (_, &MyData) = ctx.get_timer_info(timer_id)?; 26 | let reply = format!("Remaining: {:?}, data: {:?}", remaining, data); 27 | 28 | Ok(reply.into()) 29 | } 30 | 31 | fn timer_stop(ctx: &Context, args: Vec) -> ValkeyResult { 32 | let mut args = args.into_iter().skip(1); 33 | let timer_id = args.next_u64()?; 34 | 35 | let data: MyData = ctx.stop_timer(timer_id)?; 36 | let reply = format!("Data: {:?}", data); 37 | 38 | Ok(reply.into()) 39 | } 40 | 41 | ////////////////////////////////////////////////////// 42 | 43 | valkey_module! { 44 | name: "timer", 45 | version: 1, 46 | allocator: (ValkeyAlloc, ValkeyAlloc), 47 | data_types: [], 48 | commands: [ 49 | ["timer.create", timer_create, "", 0, 0, 0], 50 | ["timer.info", timer_info, "", 0, 0, 0], 51 | ["timer.stop", timer_stop, "", 0, 0, 0], 52 | ], 53 | } 54 | -------------------------------------------------------------------------------- /sbin/system-setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import os 5 | import argparse 6 | 7 | HERE = os.path.abspath(os.path.dirname(__file__)) 8 | ROOT = os.path.abspath(os.path.join(HERE, "..")) 9 | READIES = os.path.join(ROOT, "deps/readies") 10 | sys.path.insert(0, READIES) 11 | import paella 12 | 13 | #---------------------------------------------------------------------------------------------- 14 | 15 | class RedisModuleRSSetup(paella.Setup): 16 | def __init__(self, args): 17 | paella.Setup.__init__(self, args.nop) 18 | 19 | def common_first(self): 20 | self.install_downloaders() 21 | self.run("%s/bin/enable-utf8" % READIES, sudo=self.os != 'macos') 22 | self.install("git") 23 | 24 | self.run("%s/bin/getclang --modern" % READIES) 25 | self.run("%s/bin/getrust" % READIES) 26 | if self.osnick == 'ol8': 27 | self.install('tar') 28 | self.run("%s/bin/getcmake --usr" % READIES) 29 | 30 | def debian_compat(self): 31 | self.run("%s/bin/getgcc" % READIES) 32 | 33 | def redhat_compat(self): 34 | self.install("redhat-lsb-core") 35 | self.run("%s/bin/getgcc --modern" % READIES) 36 | 37 | if not self.platform.is_arm(): 38 | self.install_linux_gnu_tar() 39 | 40 | def fedora(self): 41 | self.run("%s/bin/getgcc" % READIES) 42 | 43 | def macos(self): 44 | self.install_gnu_utils() 45 | self.run("%s/bin/getredis -v 6" % READIES) 46 | 47 | def common_last(self): 48 | pass 49 | 50 | #---------------------------------------------------------------------------------------------- 51 | 52 | parser = argparse.ArgumentParser(description='Set up system for build.') 53 | parser.add_argument('-n', '--nop', action="store_true", help='no operation') 54 | args = parser.parse_args() 55 | 56 | RedisModuleRSSetup(args).setup() 57 | -------------------------------------------------------------------------------- /examples/acl.rs: -------------------------------------------------------------------------------- 1 | use valkey_module::alloc::ValkeyAlloc; 2 | use valkey_module::{ 3 | valkey_module, AclPermissions, Context, NextArg, ValkeyError, ValkeyResult, ValkeyString, 4 | ValkeyValue, VALKEY_OK, 5 | }; 6 | 7 | fn verify_key_access_for_user(ctx: &Context, args: Vec) -> ValkeyResult { 8 | let mut args = args.into_iter().skip(1); 9 | let user = args.next_arg()?; 10 | let key = args.next_arg()?; 11 | let res = ctx.acl_check_key_permission(&user, &key, &AclPermissions::all()); 12 | if let Err(err) = res { 13 | return Err(ValkeyError::String(format!("Err {err}"))); 14 | } 15 | Ok(ValkeyValue::SimpleStringStatic("OK")) 16 | } 17 | 18 | fn get_current_user(ctx: &Context, _args: Vec) -> ValkeyResult { 19 | Ok(ValkeyValue::BulkValkeyString(ctx.get_current_user())) 20 | } 21 | 22 | fn custom_category(_ctx: &Context, _args: Vec) -> ValkeyResult { 23 | VALKEY_OK 24 | } 25 | fn custom_categories(_ctx: &Context, _args: Vec) -> ValkeyResult { 26 | VALKEY_OK 27 | } 28 | fn existing_categories(_ctx: &Context, _args: Vec) -> ValkeyResult { 29 | VALKEY_OK 30 | } 31 | 32 | ////////////////////////////////////////////////////// 33 | 34 | valkey_module! { 35 | name: "acl", 36 | version: 1, 37 | allocator: (ValkeyAlloc, ValkeyAlloc), 38 | data_types: [], 39 | acl_categories: [ 40 | "custom_acl_one", 41 | "custom_acl_two" 42 | ] 43 | commands: [ 44 | ["verify_key_access_for_user", verify_key_access_for_user, "", 0, 0, 0], 45 | ["get_current_user", get_current_user, "", 0, 0, 0], 46 | ["custom_category", custom_category, "write", 0, 0, 0, "custom_acl_one"], 47 | ["custom_categories", custom_categories, "", 0, 0, 0, "custom_acl_one custom_acl_two"], 48 | ["existing_categories", existing_categories, "write", 0, 0, 0, "read fast admin"], 49 | ], 50 | } 51 | -------------------------------------------------------------------------------- /examples/open_key_with_flags.rs: -------------------------------------------------------------------------------- 1 | use valkey_module::alloc::ValkeyAlloc; 2 | use valkey_module::{ 3 | key::KeyFlags, valkey_module, Context, NextArg, ValkeyError, ValkeyResult, ValkeyString, 4 | ValkeyValue, 5 | }; 6 | use valkey_module_macros::command; 7 | 8 | #[command( 9 | { 10 | name: "open_key_with_flags.read", 11 | flags: [Write, DenyOOM], 12 | arity: 2, 13 | key_spec: [ 14 | { 15 | flags: [ReadOnly, Access], 16 | begin_search: Index({ index : 1 }), 17 | find_keys: Range({ last_key : 1, steps : 1, limit : 1}), 18 | } 19 | ] 20 | 21 | } 22 | )] 23 | fn read(ctx: &Context, args: Vec) -> ValkeyResult { 24 | if args.len() < 2 { 25 | return Err(ValkeyError::WrongArity); 26 | } 27 | 28 | let mut args = args.into_iter().skip(1); 29 | let key_name = args.next_arg()?; 30 | let _ = ctx.open_key_with_flags(&key_name, KeyFlags::NOEFFECTS); 31 | Ok(ValkeyValue::SimpleStringStatic("OK")) 32 | } 33 | 34 | #[command( 35 | { 36 | name: "open_key_with_flags.write", 37 | flags: [Write, DenyOOM], 38 | arity: 2, 39 | key_spec: [ 40 | { 41 | flags: [ReadWrite, Access], 42 | begin_search: Index({ index : 1 }), 43 | find_keys: Range({ last_key : 1, steps : 1, limit : 1}), 44 | } 45 | ] 46 | 47 | } 48 | )] 49 | fn write(ctx: &Context, args: Vec) -> ValkeyResult { 50 | if args.len() < 2 { 51 | return Err(ValkeyError::WrongArity); 52 | } 53 | 54 | let mut args = args.into_iter().skip(1); 55 | let key_name = args.next_arg()?; 56 | let _ = ctx.open_key_writable_with_flags(&key_name, KeyFlags::NOEFFECTS); 57 | Ok(ValkeyValue::SimpleStringStatic("OK")) 58 | } 59 | 60 | ////////////////////////////////////////////////////// 61 | 62 | valkey_module! { 63 | name: "open_key_with_flags", 64 | version: 1, 65 | allocator: (ValkeyAlloc, ValkeyAlloc), 66 | data_types: [], 67 | commands: [], 68 | } 69 | -------------------------------------------------------------------------------- /examples/test_helper.rs: -------------------------------------------------------------------------------- 1 | use valkey_module::alloc::ValkeyAlloc; 2 | use valkey_module::InfoContext; 3 | use valkey_module::{valkey_module, Context, ValkeyError, ValkeyResult, ValkeyString}; 4 | 5 | fn test_helper_version(ctx: &Context, _args: Vec) -> ValkeyResult { 6 | let ver = ctx.get_server_version()?; 7 | let response: Vec = vec![ver.major.into(), ver.minor.into(), ver.patch.into()]; 8 | 9 | Ok(response.into()) 10 | } 11 | 12 | fn test_helper_version_rm_call(ctx: &Context, _args: Vec) -> ValkeyResult { 13 | let ver = ctx.get_server_version_rm_call()?; 14 | let response: Vec = vec![ver.major.into(), ver.minor.into(), ver.patch.into()]; 15 | 16 | Ok(response.into()) 17 | } 18 | 19 | fn test_helper_command_name(ctx: &Context, _args: Vec) -> ValkeyResult { 20 | Ok(ctx.current_command_name()?.into()) 21 | } 22 | 23 | fn test_helper_err(_ctx: &Context, args: Vec) -> ValkeyResult { 24 | if args.len() < 2 { 25 | return Err(ValkeyError::WrongArity); 26 | } 27 | 28 | let msg = args.get(1).unwrap(); 29 | 30 | Err(ValkeyError::Str(msg.try_as_str().unwrap())) 31 | } 32 | 33 | fn add_info_impl(ctx: &InfoContext, _for_crash_report: bool) -> ValkeyResult<()> { 34 | ctx.builder() 35 | .add_section("test_helper") 36 | .field("field", "value")? 37 | .build_section()? 38 | .build_info() 39 | .map(|_| ()) 40 | } 41 | 42 | fn add_info(ctx: &InfoContext, for_crash_report: bool) { 43 | add_info_impl(ctx, for_crash_report).expect("Failed to add info"); 44 | } 45 | 46 | ////////////////////////////////////////////////////// 47 | 48 | valkey_module! { 49 | name: "test_helper", 50 | version: 1, 51 | allocator: (ValkeyAlloc, ValkeyAlloc), 52 | data_types: [], 53 | info: add_info, 54 | commands: [ 55 | ["test_helper.version", test_helper_version, "", 0, 0, 0], 56 | ["test_helper._version_rm_call", test_helper_version_rm_call, "", 0, 0, 0], 57 | ["test_helper.name", test_helper_command_name, "", 0, 0, 0], 58 | ["test_helper.err", test_helper_err, "", 0, 0, 0], 59 | ], 60 | } 61 | -------------------------------------------------------------------------------- /src/rediserror.rs: -------------------------------------------------------------------------------- 1 | use crate::context::call_reply::{ErrorCallReply, ErrorReply}; 2 | pub use crate::raw; 3 | use std::ffi::CStr; 4 | use std::fmt; 5 | 6 | #[derive(Debug)] 7 | pub enum ValkeyError { 8 | WrongArity, 9 | Str(&'static str), 10 | String(String), 11 | WrongType, 12 | } 13 | 14 | impl<'root> From> for ValkeyError { 15 | fn from(err: ErrorCallReply<'root>) -> Self { 16 | ValkeyError::String( 17 | err.to_utf8_string() 18 | .unwrap_or("can not convert error into String".into()), 19 | ) 20 | } 21 | } 22 | 23 | impl<'root> From> for ValkeyError { 24 | fn from(err: ErrorReply<'root>) -> Self { 25 | ValkeyError::String( 26 | err.to_utf8_string() 27 | .unwrap_or("can not convert error into String".into()), 28 | ) 29 | } 30 | } 31 | 32 | impl ValkeyError { 33 | #[must_use] 34 | pub const fn nonexistent_key() -> Self { 35 | Self::Str("ERR could not perform this operation on a key that doesn't exist") 36 | } 37 | 38 | #[must_use] 39 | pub const fn short_read() -> Self { 40 | Self::Str("ERR short read or OOM loading DB") 41 | } 42 | } 43 | 44 | impl From for ValkeyError { 45 | fn from(e: T) -> Self { 46 | Self::String(format!("ERR {e}")) 47 | } 48 | } 49 | 50 | impl fmt::Display for ValkeyError { 51 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 52 | let d = match self { 53 | Self::WrongArity => "Wrong Arity", 54 | // remove NUL from the end of raw::REDISMODULE_ERRORMSG_WRONGTYPE 55 | // before converting &[u8] to &str to ensure CString::new() doesn't 56 | // panic when this is passed to it. 57 | Self::WrongType => std::str::from_utf8( 58 | CStr::from_bytes_with_nul(raw::REDISMODULE_ERRORMSG_WRONGTYPE) 59 | .unwrap() 60 | .to_bytes(), 61 | ) 62 | .unwrap(), 63 | Self::Str(s) => s, 64 | Self::String(s) => s.as_str(), 65 | }; 66 | 67 | write!(f, "{d}") 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/context/keys_cursor.rs: -------------------------------------------------------------------------------- 1 | use crate::context::Context; 2 | use crate::key::ValkeyKey; 3 | use crate::raw; 4 | use crate::redismodule::ValkeyString; 5 | use std::ffi::c_void; 6 | use std::ptr::NonNull; 7 | 8 | pub struct KeysCursor { 9 | inner_cursor: *mut raw::RedisModuleScanCursor, 10 | } 11 | 12 | extern "C" fn scan_callback)>( 13 | ctx: *mut raw::RedisModuleCtx, 14 | key_name: *mut raw::RedisModuleString, 15 | key: *mut raw::RedisModuleKey, 16 | private_data: *mut ::std::os::raw::c_void, 17 | ) { 18 | let context = Context::new(ctx); 19 | let key_name = ValkeyString::new(NonNull::new(ctx), key_name); 20 | let redis_key = if key.is_null() { 21 | None 22 | } else { 23 | Some(ValkeyKey::from_raw_parts(ctx, key)) 24 | }; 25 | let callback = unsafe { &mut *(private_data.cast::()) }; 26 | callback(&context, key_name, redis_key.as_ref()); 27 | 28 | // we are not the owner of the key, so we must take the underline *mut raw::RedisModuleKey so it will not be freed. 29 | redis_key.map(|v| v.take()); 30 | } 31 | 32 | impl KeysCursor { 33 | pub fn new() -> Self { 34 | let inner_cursor = unsafe { raw::RedisModule_ScanCursorCreate.unwrap()() }; 35 | Self { inner_cursor } 36 | } 37 | 38 | pub fn scan)>( 39 | &self, 40 | ctx: &Context, 41 | callback: &F, 42 | ) -> bool { 43 | let res = unsafe { 44 | raw::RedisModule_Scan.unwrap()( 45 | ctx.ctx, 46 | self.inner_cursor, 47 | Some(scan_callback::), 48 | callback as *const F as *mut c_void, 49 | ) 50 | }; 51 | res != 0 52 | } 53 | 54 | pub fn restart(&self) { 55 | unsafe { raw::RedisModule_ScanCursorRestart.unwrap()(self.inner_cursor) }; 56 | } 57 | } 58 | 59 | impl Default for KeysCursor { 60 | fn default() -> Self { 61 | Self::new() 62 | } 63 | } 64 | 65 | impl Drop for KeysCursor { 66 | fn drop(&mut self) { 67 | unsafe { raw::RedisModule_ScanCursorDestroy.unwrap()(self.inner_cursor) }; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /examples/response.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{BTreeMap, BTreeSet}; 2 | use valkey_module::alloc::ValkeyAlloc; 3 | use valkey_module::{ 4 | redisvalue::ValkeyValueKey, valkey_module, Context, NextArg, ValkeyError, ValkeyResult, 5 | ValkeyString, ValkeyValue, 6 | }; 7 | 8 | fn map_mget(ctx: &Context, args: Vec) -> ValkeyResult { 9 | if args.len() < 2 { 10 | return Err(ValkeyError::WrongArity); 11 | } 12 | 13 | let mut args = args.into_iter().skip(1); 14 | let key_name = args.next_arg()?; 15 | 16 | let fields: Vec = args.collect(); 17 | 18 | let key = ctx.open_key(&key_name); 19 | let values = key.hash_get_multi(&fields)?; 20 | let res = match values { 21 | None => ValkeyValue::Null, 22 | Some(values) => { 23 | let mut map: BTreeMap = BTreeMap::new(); 24 | for (field, value) in values.into_iter() { 25 | map.insert( 26 | ValkeyValueKey::BulkValkeyString(field), 27 | ValkeyValue::BulkValkeyString(value), 28 | ); 29 | } 30 | ValkeyValue::OrderedMap(map) 31 | } 32 | }; 33 | 34 | Ok(res) 35 | } 36 | 37 | fn map_unique(ctx: &Context, args: Vec) -> ValkeyResult { 38 | if args.len() < 2 { 39 | return Err(ValkeyError::WrongArity); 40 | } 41 | 42 | let mut args = args.into_iter().skip(1); 43 | let key_name = args.next_arg()?; 44 | 45 | let fields: Vec = args.collect(); 46 | 47 | let key = ctx.open_key(&key_name); 48 | let values = key.hash_get_multi(&fields)?; 49 | let res = match values { 50 | None => ValkeyValue::Null, 51 | Some(values) => { 52 | let mut set: BTreeSet = BTreeSet::new(); 53 | for (_, value) in values.into_iter() { 54 | set.insert(ValkeyValueKey::BulkValkeyString(value)); 55 | } 56 | ValkeyValue::OrderedSet(set) 57 | } 58 | }; 59 | 60 | Ok(res) 61 | } 62 | 63 | ////////////////////////////////////////////////////// 64 | 65 | valkey_module! { 66 | name: "response", 67 | version: 1, 68 | allocator: (ValkeyAlloc, ValkeyAlloc), 69 | data_types: [], 70 | commands: [ 71 | ["map.mget", map_mget, "readonly", 1, 1, 1], 72 | ["map.unique", map_unique, "readonly", 1, 1, 1], 73 | ], 74 | } 75 | -------------------------------------------------------------------------------- /src/native_types.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::ffi::CString; 3 | use std::ptr; 4 | 5 | use crate::raw; 6 | 7 | pub struct ValkeyType { 8 | name: &'static str, 9 | version: i32, 10 | type_methods: raw::RedisModuleTypeMethods, 11 | pub raw_type: RefCell<*mut raw::RedisModuleType>, 12 | } 13 | 14 | // We want to be able to create static instances of this type, 15 | // which means we need to implement Sync. 16 | unsafe impl Sync for ValkeyType {} 17 | 18 | impl ValkeyType { 19 | #[must_use] 20 | pub const fn new( 21 | name: &'static str, 22 | version: i32, 23 | type_methods: raw::RedisModuleTypeMethods, 24 | ) -> Self { 25 | Self { 26 | name, 27 | version, 28 | type_methods, 29 | raw_type: RefCell::new(ptr::null_mut()), 30 | } 31 | } 32 | 33 | #[allow(clippy::not_unsafe_ptr_arg_deref)] 34 | pub fn create_data_type(&self, ctx: *mut raw::RedisModuleCtx) -> Result<(), &str> { 35 | if self.name.len() != 9 { 36 | let msg = "Valkey requires the length of native type names to be exactly 9 characters"; 37 | redis_log(ctx, format!("{msg}, name is: '{}'", self.name).as_str()); 38 | return Err(msg); 39 | } 40 | 41 | let type_name = CString::new(self.name).unwrap(); 42 | 43 | let redis_type = unsafe { 44 | raw::RedisModule_CreateDataType.unwrap()( 45 | ctx, 46 | type_name.as_ptr(), 47 | self.version, // Encoding version 48 | &mut self.type_methods.clone(), 49 | ) 50 | }; 51 | 52 | if redis_type.is_null() { 53 | redis_log(ctx, "Error: created data type is null"); 54 | return Err("Error: created data type is null"); 55 | } 56 | 57 | *self.raw_type.borrow_mut() = redis_type; 58 | 59 | redis_log( 60 | ctx, 61 | format!("Created new data type '{}'", self.name).as_str(), 62 | ); 63 | 64 | Ok(()) 65 | } 66 | } 67 | 68 | // TODO: Move to raw 69 | #[allow(clippy::not_unsafe_ptr_arg_deref)] 70 | pub fn redis_log(ctx: *mut raw::RedisModuleCtx, msg: &str) { 71 | let level = CString::new("notice").unwrap(); // FIXME reuse this 72 | let msg = CString::new(msg).unwrap(); 73 | unsafe { 74 | raw::RedisModule_Log.unwrap()(ctx, level.as_ptr(), msg.as_ptr()); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /.github/workflows/cratesio-publish.yml: -------------------------------------------------------------------------------- 1 | name: Cratesio Publish 2 | on: 3 | release: 4 | types: [published] 5 | 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions-rs/toolchain@v1 13 | with: 14 | toolchain: stable 15 | override: true 16 | 17 | - name: get version from tag 18 | id: get_version 19 | run: | 20 | realversion="${GITHUB_REF/refs\/tags\//}" 21 | realversion="${realversion//v/}" 22 | echo "::set-output name=VERSION::$realversion" 23 | 24 | - name: Set the version for publishing 25 | uses: ciiiii/toml-editor@1.0.0 26 | with: 27 | file: "Cargo.toml" 28 | key: "package.version" 29 | value: "${{ steps.get_version.outputs.VERSION }}" 30 | 31 | - name: Set the version for redismodule-rs-macros-internals 32 | uses: ciiiii/toml-editor@1.0.0 33 | with: 34 | file: "Cargo.toml" 35 | key: "dependencies.redis-module-macros-internals" 36 | value: "${{ steps.get_version.outputs.VERSION }}" 37 | 38 | - name: Set the version for publishing on macros crate 39 | uses: ciiiii/toml-editor@1.0.0 40 | with: 41 | file: "redismodule-rs-macros/Cargo.toml" 42 | key: "package.version" 43 | value: "${{ steps.get_version.outputs.VERSION }}" 44 | 45 | - name: Set the version for publishing on internal macros crate 46 | uses: ciiiii/toml-editor@1.0.0 47 | with: 48 | file: "redismodule-rs-macros-internals/Cargo.toml" 49 | key: "package.version" 50 | value: "${{ steps.get_version.outputs.VERSION }}" 51 | 52 | - name: Publishing redismodule-rs-macros-internals 53 | uses: katyo/publish-crates@v1 54 | with: 55 | registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} 56 | path: './redismodule-rs-macros-internals' 57 | args: --allow-dirty 58 | 59 | - name: Publishing redismodule-rs 60 | uses: katyo/publish-crates@v1 61 | with: 62 | registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} 63 | args: --allow-dirty 64 | 65 | - name: Publishing redismodule-rs-macros 66 | uses: katyo/publish-crates@v1 67 | with: 68 | registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} 69 | path: './redismodule-rs-macros' 70 | args: --allow-dirty 71 | -------------------------------------------------------------------------------- /examples/threads.rs: -------------------------------------------------------------------------------- 1 | use lazy_static::lazy_static; 2 | use std::mem::drop; 3 | use std::thread; 4 | use std::time::Duration; 5 | use valkey_module::alloc::ValkeyAlloc; 6 | use valkey_module::{ 7 | valkey_module, Context, NextArg, ThreadSafeContext, ValkeyGILGuard, ValkeyResult, ValkeyString, 8 | ValkeyValue, 9 | }; 10 | 11 | fn threads(_: &Context, _args: Vec) -> ValkeyResult { 12 | thread::spawn(move || { 13 | let thread_ctx = ThreadSafeContext::new(); 14 | 15 | loop { 16 | let ctx = thread_ctx.lock(); 17 | ctx.call("INCR", &["threads"]).unwrap(); 18 | // release the lock as soon as we're done accessing valkey memory 19 | drop(ctx); 20 | thread::sleep(Duration::from_millis(1000)); 21 | } 22 | }); 23 | 24 | Ok(().into()) 25 | } 26 | 27 | #[derive(Default)] 28 | struct StaticData { 29 | data: String, 30 | } 31 | 32 | lazy_static! { 33 | static ref STATIC_DATA: ValkeyGILGuard = ValkeyGILGuard::new(StaticData::default()); 34 | } 35 | 36 | fn set_static_data(ctx: &Context, args: Vec) -> ValkeyResult { 37 | let mut args = args.into_iter().skip(1); 38 | let val = args.next_str()?; 39 | let mut static_data = STATIC_DATA.lock(ctx); 40 | static_data.data = val.to_string(); 41 | Ok(ValkeyValue::SimpleStringStatic("OK")) 42 | } 43 | 44 | fn get_static_data(ctx: &Context, _args: Vec) -> ValkeyResult { 45 | let static_data = STATIC_DATA.lock(ctx); 46 | Ok(ValkeyValue::BulkString(static_data.data.clone())) 47 | } 48 | 49 | fn get_static_data_on_thread(ctx: &Context, _args: Vec) -> ValkeyResult { 50 | let blocked_client = ctx.block_client(); 51 | let _ = thread::spawn(move || { 52 | let thread_ctx = ThreadSafeContext::with_blocked_client(blocked_client); 53 | let ctx = thread_ctx.lock(); 54 | let static_data = STATIC_DATA.lock(&ctx); 55 | thread_ctx.reply(Ok(static_data.data.clone().into())); 56 | }); 57 | 58 | Ok(ValkeyValue::NoReply) 59 | } 60 | 61 | ////////////////////////////////////////////////////// 62 | 63 | valkey_module! { 64 | name: "threads", 65 | version: 1, 66 | allocator: (ValkeyAlloc, ValkeyAlloc), 67 | data_types: [], 68 | commands: [ 69 | ["threads", threads, "", 0, 0, 0], 70 | ["set_static_data", set_static_data, "", 0, 0, 0], 71 | ["get_static_data", get_static_data, "", 0, 0, 0], 72 | ["get_static_data_on_thread", get_static_data_on_thread, "", 0, 0, 0], 73 | ], 74 | } 75 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use crate::ValkeyError; 2 | use std::error; 3 | use std::fmt; 4 | use std::fmt::Display; 5 | 6 | #[derive(Debug)] 7 | pub enum Error { 8 | Generic(GenericError), 9 | FromUtf8(std::string::FromUtf8Error), 10 | ParseInt(std::num::ParseIntError), 11 | } 12 | 13 | impl Error { 14 | #[must_use] 15 | pub fn generic(message: &str) -> Self { 16 | Self::Generic(GenericError::new(message)) 17 | } 18 | } 19 | 20 | impl From for Error { 21 | fn from(err: ValkeyError) -> Self { 22 | Self::generic(err.to_string().as_str()) 23 | } 24 | } 25 | 26 | impl From for Error { 27 | fn from(err: std::string::FromUtf8Error) -> Self { 28 | Self::FromUtf8(err) 29 | } 30 | } 31 | 32 | impl From for Error { 33 | fn from(err: std::num::ParseIntError) -> Self { 34 | Self::ParseInt(err) 35 | } 36 | } 37 | 38 | impl Display for Error { 39 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 40 | match *self { 41 | // Both underlying errors already impl `Display`, so we defer to 42 | // their implementations. 43 | Self::Generic(ref err) => write!(f, "{err}"), 44 | Self::FromUtf8(ref err) => write!(f, "{err}"), 45 | Self::ParseInt(ref err) => write!(f, "{err}"), 46 | } 47 | } 48 | } 49 | 50 | impl error::Error for Error { 51 | fn cause(&self) -> Option<&dyn error::Error> { 52 | match *self { 53 | // N.B. Both of these implicitly cast `err` from their concrete 54 | // types (either `&io::Error` or `&num::ParseIntError`) 55 | // to a trait object `&Error`. This works because both error types 56 | // implement `Error`. 57 | Self::Generic(ref err) => Some(err), 58 | Self::FromUtf8(ref err) => Some(err), 59 | Self::ParseInt(ref err) => Some(err), 60 | } 61 | } 62 | } 63 | 64 | #[derive(Debug)] 65 | pub struct GenericError { 66 | message: String, 67 | } 68 | 69 | impl GenericError { 70 | #[must_use] 71 | pub fn new(message: &str) -> Self { 72 | Self { 73 | message: String::from(message), 74 | } 75 | } 76 | } 77 | 78 | impl Display for GenericError { 79 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 80 | write!(f, "Store error: {}", self.message) 81 | } 82 | } 83 | 84 | impl error::Error for GenericError { 85 | fn description(&self) -> &str { 86 | self.message.as_str() 87 | } 88 | 89 | fn cause(&self) -> Option<&dyn error::Error> { 90 | None 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ROOT=. 2 | 3 | include $(ROOT)/deps/readies/mk/main 4 | 5 | #---------------------------------------------------------------------------------------------- 6 | 7 | define HELPTEXT 8 | make build 9 | DEBUG=1 # build debug variant 10 | make clean # remove binary files 11 | ALL=1 # remove binary directories 12 | 13 | make all # build all libraries and packages 14 | 15 | make test # run tests 16 | 17 | make docker # build for specific Linux distribution 18 | OSNICK=nick # Linux distribution to build for 19 | REDIS_VER=ver # use Redis version `ver` 20 | TEST=1 # test aftar build 21 | 22 | 23 | endef 24 | 25 | #---------------------------------------------------------------------------------------------- 26 | 27 | MK_CUSTOM_CLEAN=1 28 | BINDIR=$(BINROOT) 29 | 30 | include $(MK)/defs 31 | include $(MK)/rules 32 | 33 | #---------------------------------------------------------------------------------------------- 34 | 35 | MODULE_NAME=valkeymodule-rs.so 36 | 37 | ifeq ($(DEBUG),1) 38 | TARGET_DIR=target/debug 39 | else 40 | CARGO_FLAGS += --release 41 | TARGET_DIR=target/release 42 | endif 43 | 44 | TARGET=$(TARGET_DIR)/$(MODULE_NAME) 45 | 46 | #---------------------------------------------------------------------------------------------- 47 | 48 | lint: 49 | cargo fmt -- --check 50 | 51 | .PHONY: lint 52 | 53 | #---------------------------------------------------------------------------------------------- 54 | 55 | RUST_SOEXT.linux=so 56 | RUST_SOEXT.freebsd=so 57 | RUST_SOEXT.macos=dylib 58 | 59 | build: 60 | cargo build --all --all-targets --no-default-features $(CARGO_FLAGS) 61 | # cp $(TARGET_DIR)/librejson.$(RUST_SOEXT.$(OS)) $(TARGET) 62 | 63 | clean: 64 | ifneq ($(ALL),1) 65 | cargo clean 66 | else 67 | rm -rf target 68 | endif 69 | 70 | .PHONY: build clean 71 | 72 | #---------------------------------------------------------------------------------------------- 73 | 74 | test: cargo_test cargo_deny 75 | 76 | cargo_deny: 77 | cargo install --locked cargo-deny 78 | cargo deny check licenses 79 | cargo deny check bans 80 | 81 | cargo_test: 82 | cargo test --workspace --no-default-features $(CARGO_FLAGS) 83 | cargo test --doc --workspace --no-default-features $(CARGO_FLAGS) 84 | 85 | .PHONY: test cargo_test cargo_deny 86 | 87 | #---------------------------------------------------------------------------------------------- 88 | 89 | docker: 90 | @make -C build/docker build 91 | 92 | info: 93 | gcc --version 94 | cmake --version 95 | clang --version 96 | rustc --version 97 | cargo --version 98 | rustup --version 99 | rustup show 100 | 101 | .PHONY: docker info 102 | -------------------------------------------------------------------------------- /src/digest.rs: -------------------------------------------------------------------------------- 1 | use crate::{raw, ValkeyString}; 2 | 3 | use std::os::raw::{c_char, c_int, c_longlong}; 4 | 5 | /// `Digest` is a high-level rust interface to the Valkey module C API 6 | /// abstracting away the raw C ffi calls. 7 | pub struct Digest { 8 | pub dig: *mut raw::RedisModuleDigest, 9 | } 10 | 11 | impl Digest { 12 | pub const fn new(dig: *mut raw::RedisModuleDigest) -> Self { 13 | Self { dig } 14 | } 15 | 16 | /// Returns the key name of this [`Digest`]. 17 | /// 18 | /// # Panics 19 | /// 20 | /// Will panic if `RedisModule_GetKeyNameFromDigest` is missing in redismodule.h 21 | pub fn get_key_name(&self) -> ValkeyString { 22 | ValkeyString::new(None, unsafe { 23 | raw::RedisModule_GetKeyNameFromDigest 24 | .expect("RedisModule_GetKeyNameFromDigest is not available.")(self.dig) 25 | .cast_mut() 26 | }) 27 | } 28 | 29 | /// Returns the database ID of this [`Digest`]. 30 | /// 31 | /// # Panics 32 | /// 33 | /// Will panic if `RedisModule_GetDbIdFromDigest` is missing in redismodule.h 34 | pub fn get_db_id(&self) -> c_int { 35 | unsafe { 36 | raw::RedisModule_GetDbIdFromDigest 37 | .expect("RedisModule_GetDbIdFromDigest is not available.")(self.dig) 38 | } 39 | } 40 | 41 | /// Adds a new element to this [`Digest`]. 42 | /// 43 | /// # Panics 44 | /// 45 | /// Will panic if `RedisModule_DigestAddStringBuffer` is missing in redismodule.h 46 | pub fn add_string_buffer(&mut self, ele: &[u8]) { 47 | unsafe { 48 | raw::RedisModule_DigestAddStringBuffer 49 | .expect("RedisModule_DigestAddStringBuffer is not available.")( 50 | self.dig, 51 | ele.as_ptr().cast::(), 52 | ele.len(), 53 | ) 54 | } 55 | } 56 | 57 | /// Similar to [`Digest::add_string_buffer`], but takes [`i64`]. 58 | /// 59 | /// # Panics 60 | /// 61 | /// Will panic if `RedisModule_DigestAddLongLong` is missing in redismodule.h 62 | pub fn add_long_long(&mut self, ll: c_longlong) { 63 | unsafe { 64 | raw::RedisModule_DigestAddLongLong 65 | .expect("RedisModule_DigestAddLongLong is not available.")(self.dig, ll) 66 | } 67 | } 68 | 69 | /// Ends the current sequence in this [`Digest`]. 70 | /// 71 | /// # Panics 72 | /// 73 | /// Will panic if `RedisModule_DigestEndSequence` is missing in redismodule.h 74 | pub fn end_sequence(&mut self) { 75 | unsafe { 76 | raw::RedisModule_DigestEndSequence 77 | .expect("RedisModule_DigestEndSequence is not available.")(self.dig) 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /unused/hello_no_macros.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::CString; 2 | 3 | extern crate libc; 4 | 5 | use libc::c_int; 6 | 7 | extern crate redis_module; 8 | 9 | use redis_module::raw; 10 | use redis_module::Context; 11 | use redis_module::{Command, ValkeyResult, ValkeyValue, RedisError}; 12 | 13 | const MODULE_NAME: &str = "hello"; 14 | const MODULE_VERSION: u32 = 1; 15 | 16 | 17 | fn hello_mul(_: &Context, args: Vec) -> ValkeyResult { 18 | if args.len() != 3 { 19 | return Err(RedisError::WrongArity); 20 | } 21 | 22 | // TODO: Write generic ValkeyValue::parse method 23 | if let ValkeyValue::Integer(m1) = parse_integer(&args[1])? { 24 | if let ValkeyValue::Integer(m2) = parse_integer(&args[2])? { 25 | let result = m1 * m2; 26 | 27 | return Ok(ValkeyValue::Array( 28 | vec![m1, m2, result] 29 | .into_iter() 30 | .map(|v| ValkeyValue::Integer(v)) 31 | .collect())); 32 | } 33 | } 34 | 35 | Err(RedisError::String("Something went wrong")) 36 | } 37 | 38 | ////////////////////////////////////////////////////// 39 | 40 | #[no_mangle] 41 | #[allow(non_snake_case)] 42 | pub extern "C" fn RedisModule_OnLoad( 43 | ctx: *mut raw::RedisModuleCtx, 44 | _argv: *mut *mut raw::RedisModuleString, 45 | _argc: c_int, 46 | ) -> c_int { 47 | unsafe { 48 | ////////////////// 49 | 50 | let module_name = MODULE_NAME; 51 | let module_version = MODULE_VERSION; 52 | 53 | let commands = [ 54 | Command::new("hello.mul", hello_mul, "write"), 55 | ]; 56 | 57 | ////////////////// 58 | 59 | let module_name = CString::new(module_name).unwrap(); 60 | let module_version = module_version as c_int; 61 | 62 | if raw::Export_RedisModule_Init( 63 | ctx, 64 | module_name.as_ptr(), 65 | module_version, 66 | raw::REDISMODULE_APIVER_1 as c_int, 67 | ) == raw::Status::Err as _ { return raw::Status::Err as _; } 68 | 69 | for command in &commands { 70 | let name = CString::new(command.name).unwrap(); 71 | let flags = CString::new(command.flags).unwrap(); 72 | let (firstkey, lastkey, keystep) = (1, 1, 1); 73 | 74 | if raw::RedisModule_CreateCommand.unwrap()( 75 | ctx, 76 | name.as_ptr(), 77 | command.wrap_handler(), 78 | flags.as_ptr(), 79 | firstkey, lastkey, keystep, 80 | ) == raw::Status::Err as _ { return raw::Status::Err as _; } 81 | } 82 | 83 | raw::Status::Ok as _ 84 | } 85 | } 86 | 87 | fn parse_integer(arg: &str) -> RedisResult { 88 | arg.parse::() 89 | .map_err(|_| RedisError::String("Couldn't parse as integer")) 90 | .map(|v| ValkeyValue::Integer(v)) 91 | //Error::generic(format!("Couldn't parse as integer: {}", arg).as_str())) 92 | } 93 | -------------------------------------------------------------------------------- /valkeymodule-rs-macros-internals/src/api_versions.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use proc_macro2::TokenStream; 4 | use quote::quote; 5 | 6 | lazy_static::lazy_static! { 7 | pub(crate) static ref API_VERSION_MAPPING: HashMap = HashMap::from([ 8 | ("RedisModule_AddPostNotificationJob".to_string(), 70200), 9 | ("RedisModule_SetCommandACLCategories".to_string(), 70200), 10 | ("RedisModule_GetOpenKeyModesAll".to_string(), 70200), 11 | ("RedisModule_CallReplyPromiseSetUnblockHandler".to_string(), 70200), 12 | ("RedisModule_CallReplyPromiseAbort".to_string(), 70200), 13 | ("RedisModule_Microseconds".to_string(), 70200), 14 | ("RedisModule_CachedMicroseconds".to_string(), 70200), 15 | ("RedisModule_RegisterAuthCallback".to_string(), 70200), 16 | ("RedisModule_BlockClientOnKeysWithFlags".to_string(), 70200), 17 | ("RedisModule_GetModuleOptionsAll".to_string(), 70200), 18 | ("RedisModule_BlockClientGetPrivateData".to_string(), 70200), 19 | ("RedisModule_BlockClientSetPrivateData".to_string(), 70200), 20 | ("RedisModule_BlockClientOnAuth".to_string(), 70200), 21 | ("RedisModule_ACLAddLogEntryByUserName".to_string(), 70200), 22 | ("RedisModule_GetCommand".to_string(), 70000), 23 | ("RedisModule_SetCommandInfo".to_string(), 70000), 24 | ("RedisModule_AddACLCategory".to_string(), 80000), 25 | ]); 26 | 27 | pub(crate) static ref API_OLDEST_VERSION: usize = 60000; 28 | pub(crate) static ref ALL_VERSIONS: Vec<(usize, String)> = vec![ 29 | (60000, "min-redis-compatibility-version-6-0".to_string()), 30 | (60200, "min-redis-compatibility-version-6-2".to_string()), 31 | (70000, "min-redis-compatibility-version-7-0".to_string()), 32 | (70200, "min-redis-compatibility-version-7-2".to_string()), 33 | (80000, "min-valkey-compatibility-version-8-0".to_string()), 34 | ]; 35 | } 36 | 37 | pub(crate) fn get_feature_flags( 38 | min_required_version: usize, 39 | ) -> (Vec, Vec) { 40 | let all_lower_versions: Vec<&str> = ALL_VERSIONS 41 | .iter() 42 | .filter_map(|(v, s)| { 43 | if *v < min_required_version { 44 | Some(s.as_str()) 45 | } else { 46 | None 47 | } 48 | }) 49 | .collect(); 50 | let all_upper_versions: Vec<&str> = ALL_VERSIONS 51 | .iter() 52 | .filter_map(|(v, s)| { 53 | if *v >= min_required_version { 54 | Some(s.as_str()) 55 | } else { 56 | None 57 | } 58 | }) 59 | .collect(); 60 | ( 61 | all_lower_versions 62 | .into_iter() 63 | .map(|s| quote!(feature = #s).into()) 64 | .collect(), 65 | all_upper_versions 66 | .into_iter() 67 | .map(|s| quote!(feature = #s).into()) 68 | .collect(), 69 | ) 70 | } 71 | -------------------------------------------------------------------------------- /examples/events.rs: -------------------------------------------------------------------------------- 1 | use std::ptr::NonNull; 2 | use std::sync::atomic::{AtomicI64, Ordering}; 3 | use valkey_module::alloc::ValkeyAlloc; 4 | use valkey_module::{ 5 | valkey_module, Context, NotifyEvent, Status, ValkeyError, ValkeyResult, ValkeyString, 6 | ValkeyValue, 7 | }; 8 | 9 | static NUM_KEY_MISSES: AtomicI64 = AtomicI64::new(0); 10 | static NUM_KEYS: AtomicI64 = AtomicI64::new(0); 11 | 12 | fn on_event(ctx: &Context, event_type: NotifyEvent, event: &str, key: &[u8]) { 13 | if key == b"num_sets" { 14 | // break infinite loop 15 | return; 16 | } 17 | let msg = format!( 18 | "Received event: {:?} on key: {} via event: {}", 19 | event_type, 20 | std::str::from_utf8(key).unwrap(), 21 | event 22 | ); 23 | ctx.log_notice(msg.as_str()); 24 | let _ = ctx.add_post_notification_job(|ctx| { 25 | // it is not safe to write inside the notification callback itself. 26 | // So we perform write on a post job notificaiton. 27 | if let Err(e) = ctx.call("incr", &["num_sets"]) { 28 | ctx.log_warning(&format!("Error on incr command, {}.", e)); 29 | } 30 | }); 31 | } 32 | 33 | fn on_stream(ctx: &Context, _event_type: NotifyEvent, _event: &str, _key: &[u8]) { 34 | ctx.log_debug("Stream event received!"); 35 | } 36 | 37 | fn event_send(ctx: &Context, args: Vec) -> ValkeyResult { 38 | if args.len() > 1 { 39 | return Err(ValkeyError::WrongArity); 40 | } 41 | 42 | let key_name = ValkeyString::create(NonNull::new(ctx.ctx), "mykey"); 43 | let status = ctx.notify_keyspace_event(NotifyEvent::GENERIC, "events.send", &key_name); 44 | match status { 45 | Status::Ok => Ok("Event sent".into()), 46 | Status::Err => Err(ValkeyError::Str("Generic error")), 47 | } 48 | } 49 | 50 | fn on_key_miss(_ctx: &Context, _event_type: NotifyEvent, _event: &str, _key: &[u8]) { 51 | NUM_KEY_MISSES.fetch_add(1, Ordering::SeqCst); 52 | } 53 | 54 | fn num_key_miss(_ctx: &Context, _args: Vec) -> ValkeyResult { 55 | Ok(ValkeyValue::Integer(NUM_KEY_MISSES.load(Ordering::SeqCst))) 56 | } 57 | 58 | fn on_new_key(_ctx: &Context, _event_type: NotifyEvent, _event: &str, _key: &[u8]) { 59 | NUM_KEYS.fetch_add(1, Ordering::SeqCst); 60 | } 61 | 62 | fn num_keys(_ctx: &Context, _args: Vec) -> ValkeyResult { 63 | Ok(ValkeyValue::Integer(NUM_KEYS.load(Ordering::SeqCst))) 64 | } 65 | ////////////////////////////////////////////////////// 66 | 67 | valkey_module! { 68 | name: "events", 69 | version: 1, 70 | allocator: (ValkeyAlloc, ValkeyAlloc), 71 | data_types: [], 72 | commands: [ 73 | ["events.send", event_send, "", 0, 0, 0], 74 | ["events.num_key_miss", num_key_miss, "", 0, 0, 0], 75 | ["events.num_keys", num_keys, "", 0, 0, 0], 76 | ], 77 | event_handlers: [ 78 | [@STRING: on_event], 79 | [@STREAM: on_stream], 80 | [@MISSED: on_key_miss], 81 | [@NEW: on_new_key], 82 | ], 83 | } 84 | -------------------------------------------------------------------------------- /examples/data_type2.rs: -------------------------------------------------------------------------------- 1 | use std::os::raw::c_void; 2 | use valkey_module::alloc::ValkeyAlloc; 3 | use valkey_module::digest::Digest; 4 | use valkey_module::native_types::ValkeyType; 5 | use valkey_module::{raw, valkey_module, Context, NextArg, ValkeyResult, ValkeyString}; 6 | 7 | #[derive(Debug)] 8 | struct MyType { 9 | data: i64, 10 | } 11 | 12 | static MY_VALKEY_TYPE: ValkeyType = ValkeyType::new( 13 | "mytype456", 14 | 0, 15 | raw::RedisModuleTypeMethods { 16 | version: raw::REDISMODULE_TYPE_METHOD_VERSION as u64, 17 | rdb_load: None, 18 | rdb_save: None, 19 | aof_rewrite: None, 20 | free: Some(free), 21 | digest: Some(digest), 22 | mem_usage: None, 23 | 24 | // Aux data 25 | aux_load: None, 26 | aux_save: None, 27 | aux_save2: None, 28 | aux_save_triggers: 0, 29 | 30 | free_effort: None, 31 | unlink: None, 32 | copy: None, 33 | defrag: None, 34 | 35 | copy2: None, 36 | free_effort2: None, 37 | mem_usage2: None, 38 | unlink2: None, 39 | }, 40 | ); 41 | 42 | unsafe extern "C" fn free(value: *mut c_void) { 43 | drop(Box::from_raw(value.cast::())); 44 | } 45 | 46 | unsafe extern "C" fn digest(md: *mut raw::RedisModuleDigest, value: *mut c_void) { 47 | let mut dig = Digest::new(md); 48 | let val = &*(value.cast::()); 49 | dig.add_long_long(val.data); 50 | dig.get_db_id(); 51 | let keyname = dig.get_key_name(); 52 | assert!(!keyname.is_empty()); 53 | dig.end_sequence(); 54 | } 55 | 56 | fn alloc_set(ctx: &Context, args: Vec) -> ValkeyResult { 57 | let mut args = args.into_iter().skip(1); 58 | let key = args.next_arg()?; 59 | let size = args.next_i64()?; 60 | 61 | ctx.log_debug(format!("key: {key}, size: {size}").as_str()); 62 | 63 | let key = ctx.open_key_writable(&key); 64 | 65 | if let Some(value) = key.get_value::(&MY_VALKEY_TYPE)? { 66 | value.data = size; 67 | } else { 68 | let value = MyType { data: size }; 69 | key.set_value(&MY_VALKEY_TYPE, value)?; 70 | } 71 | Ok(size.into()) 72 | } 73 | 74 | fn alloc_get(ctx: &Context, args: Vec) -> ValkeyResult { 75 | let mut args = args.into_iter().skip(1); 76 | let key = args.next_arg()?; 77 | 78 | let key = ctx.open_key(&key); 79 | 80 | let value = match key.get_value::(&MY_VALKEY_TYPE)? { 81 | Some(value) => value.data.into(), 82 | None => ().into(), 83 | }; 84 | 85 | Ok(value) 86 | } 87 | 88 | ////////////////////////////////////////////////////// 89 | 90 | valkey_module! { 91 | name: "alloc2", 92 | version: 1, 93 | allocator: (ValkeyAlloc, ValkeyAlloc), 94 | data_types: [ 95 | MY_VALKEY_TYPE, 96 | ], 97 | commands: [ 98 | ["alloc2.set", alloc_set, "write", 1, 1, 1], 99 | ["alloc2.get", alloc_get, "readonly", 1, 1, 1], 100 | ], 101 | } 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![license](https://img.shields.io/github/license/RedisLabsModules/redismodule-rs.svg)](https://github.com/valkey-io/valkeymodule-rs/blob/main/LICENSE) 2 | [![Releases](https://img.shields.io/github/release/RedisLabsModules/redismodule-rs.svg)](https://github.com/valkey-io/valkeymodule-rs/releases) 3 | [![crates.io](https://img.shields.io/crates/v/redis-module.svg)](https://crates.io/crates/valkey-module) 4 | [![docs](https://docs.rs/redis-module/badge.svg)](https://docs.rs/valkey-module) 5 | [![CircleCI](https://circleci.com/gh/RedisLabsModules/redismodule-rs/tree/master.svg?style=svg)](https://circleci.com/gh/RedisLabsModules/redismodule-rs/tree/master) 6 | 7 | # valkeymodule-rs 8 | 9 | This crate provides an idiomatic Rust API for the [Valkey Modules API](https://valkey.io/topics/modules-api-ref/). 10 | It allows writing Valkey modules in Rust, without needing to use raw pointers or unsafe code. See [here](https://docs.rs/valkey-module/latest) for the most recent API documentation. 11 | 12 | This repo was forked from [redismodule-rs](https://github.com/RedisLabsModules/redismodule-rs). We appreciate the contributions of the original authors. 13 | 14 | # Running the example module 15 | 16 | 1. [Install Rust](https://www.rust-lang.org/tools/install) 17 | 2. [Install Valkey](https://valkey.io/download/), most likely using your favorite package manager (Homebrew on Mac, APT or YUM on Linux) 18 | 3. Run `cargo build --example hello` 19 | 4. Start a valkey server with the `hello` module 20 | * Linux: `valkey-server --loadmodule ./target/debug/examples/libhello.so` 21 | * Mac: `valkey-server --loadmodule ./target/debug/examples/libhello.dylib` 22 | 5. Open a valkey-cli, and run `HELLO.MUL 31 11`. 23 | 24 | # Writing your own module 25 | 26 | See the [examples](examples) directory for some sample modules. 27 | 28 | This crate tries to provide high-level wrappers around the standard Valkey Modules API, while preserving the API's basic concepts. 29 | Therefore, following the [Valkey Modules API](https://valkey.io/topics/modules-api-ref/) documentation will be mostly relevant here as well. 30 | 31 | ## Feature Flags 32 | 33 | 1. System Allocator 34 | 35 | This feature flag is ideal for unit testing where the engine server is not running, and we do not have access to the Vakey engine Allocator; so we can use the System Allocator instead. 36 | To optionally enter the `System.alloc` code paths in `alloc.rs` specify this in `Cargo.toml` of your module: 37 | ``` 38 | [features] 39 | enable-system-alloc = ["valkey-module/system-alloc"] 40 | ``` 41 | For unit tests with `System.alloc` use this: 42 | ``` 43 | cargo test --features enable-system-alloc 44 | ``` 45 | For integration tests with `ValkeyAlloc` use this: 46 | ``` 47 | cargo test 48 | ``` 49 | 50 | 2. Redis Compatibility 51 | 52 | This feature flag is useful in case you have a Module that needs to be loaded on both Valkey and Redis Servers. In this case, you can use the `use-redismodule-api` flag so that the Module is loaded using the RedisModule API Initialization for compatibility. 53 | 54 | To use this feature by conditionally, specify the following in your `Cargo.toml`: 55 | ``` 56 | [features] 57 | use-redismodule-api = ["valkey-module/use-redismodule-api"] 58 | default = [] 59 | ``` 60 | 61 | ``` 62 | cargo build --release --features use-redismodule-api 63 | ``` 64 | -------------------------------------------------------------------------------- /src/stream.rs: -------------------------------------------------------------------------------- 1 | use crate::key::ValkeyKey; 2 | use crate::raw; 3 | use crate::Status; 4 | use crate::ValkeyError; 5 | use crate::ValkeyString; 6 | use std::os::raw::c_long; 7 | use std::ptr; 8 | 9 | #[derive(Debug)] 10 | pub struct StreamRecord { 11 | pub id: raw::RedisModuleStreamID, 12 | pub fields: Vec<(ValkeyString, ValkeyString)>, 13 | } 14 | 15 | #[derive(Debug)] 16 | pub struct StreamIterator<'key> { 17 | key: &'key ValkeyKey, 18 | } 19 | 20 | impl<'key> StreamIterator<'key> { 21 | pub(crate) fn new( 22 | key: &ValkeyKey, 23 | mut from: Option, 24 | mut to: Option, 25 | exclusive: bool, 26 | reverse: bool, 27 | ) -> Result { 28 | let mut flags = if exclusive { 29 | raw::REDISMODULE_STREAM_ITERATOR_EXCLUSIVE as i32 30 | } else { 31 | 0 32 | }; 33 | 34 | flags |= if reverse { 35 | raw::REDISMODULE_STREAM_ITERATOR_REVERSE as i32 36 | } else { 37 | 0 38 | }; 39 | 40 | let res = unsafe { 41 | raw::RedisModule_StreamIteratorStart.unwrap()( 42 | key.key_inner, 43 | flags, 44 | from.as_mut().map_or(ptr::null_mut(), |v| v), 45 | to.as_mut().map_or(ptr::null_mut(), |v| v), 46 | ) 47 | }; 48 | if Status::Ok == res.into() { 49 | Ok(StreamIterator { key }) 50 | } else { 51 | Err(ValkeyError::Str("Failed creating stream iterator")) 52 | } 53 | } 54 | } 55 | 56 | impl<'key> Iterator for StreamIterator<'key> { 57 | type Item = StreamRecord; 58 | 59 | fn next(&mut self) -> Option { 60 | let mut id = raw::RedisModuleStreamID { ms: 0, seq: 0 }; 61 | let mut num_fields: c_long = 0; 62 | let mut field_name: *mut raw::RedisModuleString = ptr::null_mut(); 63 | let mut field_val: *mut raw::RedisModuleString = ptr::null_mut(); 64 | if Status::Ok 65 | != unsafe { 66 | raw::RedisModule_StreamIteratorNextID.unwrap()( 67 | self.key.key_inner, 68 | &mut id, 69 | &mut num_fields, 70 | ) 71 | } 72 | .into() 73 | { 74 | return None; 75 | } 76 | let mut fields = Vec::new(); 77 | while Status::Ok 78 | == unsafe { 79 | raw::RedisModule_StreamIteratorNextField.unwrap()( 80 | self.key.key_inner, 81 | &mut field_name, 82 | &mut field_val, 83 | ) 84 | .into() 85 | } 86 | { 87 | fields.push(( 88 | ValkeyString::from_redis_module_string(ptr::null_mut(), field_name), 89 | ValkeyString::from_redis_module_string(ptr::null_mut(), field_val), 90 | )); 91 | } 92 | Some(StreamRecord { id, fields }) 93 | } 94 | } 95 | 96 | impl<'key> Drop for StreamIterator<'key> { 97 | fn drop(&mut self) { 98 | unsafe { raw::RedisModule_StreamIteratorDelete.unwrap()(self.key.key_inner) }; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/defrag.rs: -------------------------------------------------------------------------------- 1 | use std::os::raw::c_void; 2 | 3 | use crate::{raw, Status}; 4 | 5 | /// `Defrag` is a high-level rust interface to the Valkey module C API 6 | /// abstracting away the raw C ffi calls. 7 | pub struct Defrag { 8 | pub defrag_ctx: *mut raw::RedisModuleDefragCtx, 9 | } 10 | 11 | impl Defrag { 12 | pub const fn new(defrag_ctx: *mut raw::RedisModuleDefragCtx) -> Self { 13 | Self { defrag_ctx } 14 | } 15 | 16 | /// # Returns a pointer to the new alloction of the data, if no defragmentation was needed a null pointer is returned 17 | /// 18 | /// # Panics 19 | /// 20 | /// Will panic if `RedisModule_DefragAlloc` is missing in redismodule.h 21 | pub unsafe fn alloc(&self, ptr: *mut c_void) -> *mut c_void { 22 | raw::RedisModule_DefragAlloc.unwrap()(self.defrag_ctx, ptr) 23 | } 24 | 25 | /// # Sets a cursor on the last item defragged so that on the next defrag cycle, the Module can resume from that position using `get_cursor`. 26 | /// # Should only be called if `should_stop_defrag` has returned `true` and the defrag callback is about to exit without fully iterating its data type. 27 | /// 28 | /// # Panics 29 | /// 30 | /// Will panic if `RedisModule_DefragCursorSet` is missing in redismodule.h 31 | pub unsafe fn set_cursor(&self, cursor: u64) -> Status { 32 | let status = raw::RedisModule_DefragCursorSet.unwrap()(self.defrag_ctx, cursor); 33 | if status as isize == raw::REDISMODULE_OK { 34 | Status::Ok 35 | } else { 36 | Status::Err 37 | } 38 | } 39 | 40 | /// # Returns the cursor value that has been previously stored using `set_cursor` 41 | /// 42 | /// # Panics 43 | /// 44 | /// Will panic if `RedisModule_DefragCursorGet` is missing in redismodule.h 45 | pub unsafe fn get_cursor(&self) -> Option { 46 | let mut cursor: u64 = 0; 47 | let status = raw::RedisModule_DefragCursorGet.unwrap()(self.defrag_ctx, &mut cursor); 48 | if status as isize == raw::REDISMODULE_OK { 49 | Some(cursor) 50 | } else { 51 | None 52 | } 53 | } 54 | 55 | /// # Returns true if the engine has been defragging for too long and the Module should need to stop. 56 | /// # Returns false otherwise for the Module to know it can continue its work. 57 | /// 58 | /// # Panics 59 | /// 60 | /// Will panic if `RedisModule_DefragShouldStop` is missing in redismodule.h 61 | pub unsafe fn should_stop_defrag(&self) -> bool { 62 | raw::RedisModule_DefragShouldStop.unwrap()(self.defrag_ctx) != 0 63 | } 64 | 65 | /// # Returns the name of the key being processed. 66 | /// # If the key name isn't available this will return NULL instead 67 | /// 68 | /// # Panics 69 | /// 70 | /// Will panic if `RedisModule_GetKeyNameFromDefragCtx` is missing in redismodule.h 71 | pub unsafe fn get_key_name_from_defrag_context(&self) -> *const raw::RedisModuleString { 72 | raw::RedisModule_GetKeyNameFromDefragCtx.unwrap()(self.defrag_ctx) 73 | } 74 | 75 | /// # Returns the database id of the key that is currently being defragged. 76 | /// # If this information isn't available it will return -1 77 | /// 78 | /// # Panics 79 | /// 80 | /// Will panic if `RedisModule_GetDbIdFromDefragCtx` is missing in redismodule.h 81 | pub unsafe fn get_db_id_from_defrag_context(&self) -> i32 { 82 | raw::RedisModule_GetDbIdFromDefragCtx.unwrap()(self.defrag_ctx) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /examples/filter2.rs: -------------------------------------------------------------------------------- 1 | use dashmap::DashMap; 2 | use std::os::raw::c_int; 3 | use std::sync::LazyLock; 4 | use valkey_module::alloc::ValkeyAlloc; 5 | use valkey_module::server_events::ClientChangeSubevent; 6 | use valkey_module::{ 7 | valkey_module, CommandFilterCtx, Context, RedisModuleCommandFilterCtx, Status, ValkeyError, 8 | ValkeyString, AUTH_HANDLED, AUTH_NOT_HANDLED, VALKEYMODULE_CMDFILTER_NOSELF, 9 | }; 10 | use valkey_module_macros::client_changed_event_handler; 11 | 12 | // filters do not have Context so we cannot access username in the filter directly 13 | // mapping client_id to username and then lookup username by client_id which is available via CommandFilterCtx 14 | 15 | // DashMap is a concurrent, thread-safe replacement for HashMap 16 | // it allows multiple readers and writers with no locking required on reads, and fine-grained locks for writes. 17 | static CLIENT_ID_USERNAME_MAP: LazyLock> = LazyLock::new(|| DashMap::new()); 18 | 19 | // fires on every client connect and disconnect event 20 | #[client_changed_event_handler] 21 | fn client_changed_event_handler(ctx: &Context, client_event: ClientChangeSubevent) { 22 | match client_event { 23 | ClientChangeSubevent::Connected => { 24 | // user has not authed yet and might not so set username as default 25 | let username = "default".to_string(); 26 | let client_id = ctx.get_client_id(); 27 | CLIENT_ID_USERNAME_MAP.insert(client_id, username); 28 | } 29 | ClientChangeSubevent::Disconnected => { 30 | // remove the client_id from the map 31 | let client_id = ctx.get_client_id(); 32 | CLIENT_ID_USERNAME_MAP.remove(&client_id); 33 | } 34 | } 35 | } 36 | 37 | // fires after client has authenticated so we know the new username 38 | fn auth_callback( 39 | ctx: &Context, 40 | username: ValkeyString, 41 | _password: ValkeyString, 42 | ) -> Result { 43 | // if needed, we can get the previous username 44 | let _username_before_auth = match ctx.get_client_username() { 45 | Ok(tmp) => tmp.to_string(), 46 | Err(_err) => "default".to_string(), 47 | }; 48 | if ctx.authenticate_client_with_acl_user(&username) == Status::Ok { 49 | let client_id = ctx.get_client_id(); 50 | // map client_id to username 51 | CLIENT_ID_USERNAME_MAP.insert(client_id, username.to_string()); 52 | return Ok(AUTH_HANDLED); 53 | } 54 | Ok(AUTH_NOT_HANDLED) 55 | } 56 | 57 | fn filter1_fn(ctx: *mut RedisModuleCommandFilterCtx) { 58 | // registered via valkey_module! macro 59 | // making sure that two modules can have the same filter fn name 60 | let cf_ctx = CommandFilterCtx::new(ctx); 61 | let client_id = cf_ctx.get_client_id(); 62 | // lookup username by client_id 63 | let _username = match CLIENT_ID_USERNAME_MAP.get(&client_id) { 64 | Some(tmp) => tmp.clone(), 65 | None => "default".to_string(), 66 | }; 67 | // do something with the username 68 | } 69 | 70 | fn filter2_fn(_ctx: *mut RedisModuleCommandFilterCtx) { 71 | // do something here, registered via valkey_module! macro 72 | // making sure that two modules can have the same filter fn name 73 | } 74 | 75 | valkey_module! { 76 | name: "filter2", 77 | version: 1, 78 | allocator: (ValkeyAlloc, ValkeyAlloc), 79 | data_types: [], 80 | auth: [auth_callback], 81 | commands: [ 82 | ], 83 | filters: [ 84 | [filter1_fn, VALKEYMODULE_CMDFILTER_NOSELF], 85 | [filter2_fn, VALKEYMODULE_CMDFILTER_NOSELF] 86 | ] 87 | } 88 | -------------------------------------------------------------------------------- /src/alloc.rs: -------------------------------------------------------------------------------- 1 | use std::alloc::{GlobalAlloc, Layout}; 2 | 3 | use crate::raw; 4 | 5 | /// Panics with a message without using an allocator. 6 | /// Useful when using the allocator should be avoided or it is 7 | /// inaccessible. The default [std::panic] performs allocations and so 8 | /// will cause a double panic without a meaningful message if the 9 | /// allocator can't be used. This function makes sure we can panic with 10 | /// a reasonable message even without the allocator working. 11 | fn allocation_free_panic(message: &'static str) -> ! { 12 | use std::os::unix::io::AsRawFd; 13 | 14 | let _ = nix::unistd::write(std::io::stderr().as_raw_fd(), message.as_bytes()); 15 | 16 | std::process::abort(); 17 | } 18 | 19 | const VALKEY_ALLOCATOR_NOT_AVAILABLE_MESSAGE: &str = 20 | "Critical error: the Valkey Allocator isn't available.\n"; 21 | 22 | /// Defines the Valkey allocator. This allocator delegates the allocation 23 | /// and deallocation tasks to the Valkey server when available, otherwise 24 | /// it panics. 25 | #[derive(Copy, Clone)] 26 | pub struct ValkeyAlloc; 27 | 28 | unsafe impl GlobalAlloc for ValkeyAlloc { 29 | unsafe fn alloc(&self, layout: Layout) -> *mut u8 { 30 | /* 31 | * To make sure the memory allocation by Valkey is aligned to the according to the layout, 32 | * we need to align the size of the allocation to the layout. 33 | * 34 | * "Memory is conceptually broken into equal-sized chunks, 35 | * where the chunk size is a power of two that is greater than the page size. 36 | * Chunks are always aligned to multiples of the chunk size. 37 | * This alignment makes it possible to find metadata for user objects very quickly." 38 | * 39 | * From: https://linux.die.net/man/3/jemalloc 40 | */ 41 | if cfg!(feature = "enable-system-alloc") { 42 | return std::alloc::System.alloc(layout); 43 | } 44 | 45 | let size = (layout.size() + layout.align() - 1) & (!(layout.align() - 1)); 46 | match raw::RedisModule_Alloc { 47 | Some(alloc) => alloc(size).cast(), 48 | None => allocation_free_panic(VALKEY_ALLOCATOR_NOT_AVAILABLE_MESSAGE), 49 | } 50 | } 51 | 52 | unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 { 53 | /* 54 | * To make sure the memory allocation by Valkey is aligned to the according to the layout, 55 | * we need to align the size of the allocation to the layout. 56 | * 57 | * "Memory is conceptually broken into equal-sized chunks, 58 | * where the chunk size is a power of two that is greater than the page size. 59 | * Chunks are always aligned to multiples of the chunk size. 60 | * This alignment makes it possible to find metadata for user objects very quickly." 61 | * 62 | * From: https://linux.die.net/man/3/jemalloc 63 | */ 64 | if cfg!(feature = "enable-system-alloc") { 65 | return std::alloc::System.alloc_zeroed(layout); 66 | } 67 | let size = (layout.size() + layout.align() - 1) & (!(layout.align() - 1)); 68 | let num_elements = size / layout.align(); 69 | match raw::RedisModule_Calloc { 70 | Some(calloc) => calloc(num_elements, layout.align()).cast(), 71 | None => allocation_free_panic(VALKEY_ALLOCATOR_NOT_AVAILABLE_MESSAGE), 72 | } 73 | } 74 | 75 | unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { 76 | if cfg!(feature = "enable-system-alloc") { 77 | return std::alloc::System.dealloc(ptr, layout); 78 | } 79 | match raw::RedisModule_Free { 80 | Some(f) => f(ptr.cast()), 81 | None => allocation_free_panic(VALKEY_ALLOCATOR_NOT_AVAILABLE_MESSAGE), 82 | }; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /examples/filter1.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Deref; 2 | use std::sync::RwLock; 3 | use valkey_module::alloc::ValkeyAlloc; 4 | use valkey_module::logging::log_notice; 5 | use valkey_module::{ 6 | valkey_module, CommandFilter, CommandFilterCtx, Context, RedisModuleCommandFilterCtx, Status, 7 | ValkeyResult, ValkeyString, VALKEYMODULE_CMDFILTER_NOSELF, 8 | }; 9 | 10 | // used to register filters using init and deinit, this is more complicated, best use the macro approach below 11 | static INFO_FILTER: RwLock> = RwLock::new(None); 12 | 13 | fn init(ctx: &Context, _args: &[ValkeyString]) -> Status { 14 | let info_filter = ctx.register_command_filter(info_filter_fn, VALKEYMODULE_CMDFILTER_NOSELF); 15 | if info_filter.is_null() { 16 | return Status::Err; 17 | } 18 | let mut info_guard = INFO_FILTER.write().unwrap(); 19 | *info_guard = Some(info_filter); 20 | 21 | Status::Ok 22 | } 23 | 24 | fn deinit(ctx: &Context) -> Status { 25 | let info_guard = INFO_FILTER.read().unwrap(); 26 | if let Some(ref info_filter) = info_guard.deref() { 27 | ctx.unregister_command_filter(info_filter); 28 | }; 29 | 30 | Status::Ok 31 | } 32 | 33 | // this is used to register and unregister the filter in init and deinit 34 | // has to be extern "C", better to use the macro approach 35 | extern "C" fn info_filter_fn(ctx: *mut RedisModuleCommandFilterCtx) { 36 | let cf_ctx = CommandFilterCtx::new(ctx); 37 | 38 | // we want the filter to be very efficient as it will be called for every command 39 | // check if there is only 1 arg, the command 40 | if cf_ctx.args_count() != 1 { 41 | return; 42 | } 43 | // check if cmd (first arg) is info 44 | let cmd = cf_ctx.arg_get_try_as_str(0).unwrap(); 45 | if !cmd.eq_ignore_ascii_case("info") { 46 | return; 47 | } 48 | // grab client_id 49 | let client_id = cf_ctx.get_client_id(); 50 | log_notice(&format!("info filter for client_id {}", client_id)); 51 | // replace info with info2 as the command name 52 | cf_ctx.arg_replace(0, "info2"); 53 | } 54 | 55 | // custom command that will replace info command 56 | fn info2(ctx: &Context, _args: Vec) -> ValkeyResult { 57 | ctx.log_notice("info2 command"); 58 | // do something different here 59 | Ok("info2\n".into()) 60 | } 61 | 62 | // this will be registered via valkey_module! macro, can be a regular Rust fn (not extern "C"). 63 | // this is the recommended approach for registering filters as it is simpler and cleaner 64 | fn set_filter_fn(ctx: *mut RedisModuleCommandFilterCtx) { 65 | let cf_ctx = CommandFilterCtx::new(ctx); 66 | 67 | if cf_ctx.args_count() != 3 { 68 | return; 69 | } 70 | // check if cmd (first arg) is set 71 | let cmd = cf_ctx.cmd_get_try_as_str().unwrap(); 72 | if !cmd.eq_ignore_ascii_case("set") { 73 | return; 74 | } 75 | let all_args = cf_ctx.get_all_args_wo_cmd(); 76 | log_notice(&format!("all_args: {:?}", all_args)); 77 | let key = cf_ctx.arg_get_try_as_str(1).unwrap(); 78 | let value = cf_ctx.arg_get_try_as_str(2).unwrap(); 79 | log_notice(&format!("set key: {}, value {}", key, value)); 80 | // delete 2nd arg key 81 | cf_ctx.arg_delete(1); 82 | // insert new key 83 | cf_ctx.arg_insert(1, "new_key"); 84 | // replace 3rd arg value 85 | cf_ctx.arg_replace(2, "new_value"); 86 | } 87 | 88 | fn filter1_fn(_ctx: *mut RedisModuleCommandFilterCtx) { 89 | // do something here, registered via valkey_module! macro 90 | } 91 | 92 | fn filter2_fn(_ctx: *mut RedisModuleCommandFilterCtx) { 93 | // do something here, registered via valkey_module! macro 94 | } 95 | 96 | valkey_module! { 97 | name: "filter1", 98 | version: 1, 99 | allocator: (ValkeyAlloc, ValkeyAlloc), 100 | data_types: [], 101 | init: init, 102 | deinit: deinit, 103 | commands: [ 104 | ["info2", info2, "readonly", 0, 0, 0], 105 | ], 106 | filters: [ 107 | // need to add paste crate to your Cargo.toml or you will get error: use of undeclared crate or module `paste` 108 | [set_filter_fn, VALKEYMODULE_CMDFILTER_NOSELF], 109 | [filter1_fn, VALKEYMODULE_CMDFILTER_NOSELF], 110 | [filter2_fn, VALKEYMODULE_CMDFILTER_NOSELF] 111 | ] 112 | } 113 | -------------------------------------------------------------------------------- /examples/subcmd.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | use valkey_module::redisvalue::ValkeyValueKey; 3 | use valkey_module::{ 4 | alloc::ValkeyAlloc, valkey_module, Context, NextArg, ValkeyError, ValkeyResult, ValkeyString, 5 | ValkeyValue, 6 | }; 7 | 8 | // top level command doesn't have logic, simply acts as a wrapper for subcommands 9 | fn cmd1(_ctx: &Context, args: Vec) -> ValkeyResult { 10 | if args.len() == 1 { 11 | // display help as default subcommand 12 | return help_subcmd(); 13 | }; 14 | let mut args = args.into_iter().skip(1); 15 | let subcmd = args.next_string()?; 16 | let args: Vec = args.collect(); 17 | match subcmd.to_lowercase().as_str() { 18 | "s1" => sub1(args), 19 | // more subcommands can be added here 20 | "info" => info_subcmd(args), 21 | "help" => help_subcmd(), 22 | _ => Err(ValkeyError::Str("invalid subcommand")), 23 | } 24 | } 25 | 26 | // can be called either with `cmd1 help` or just `cmd1` 27 | fn help_subcmd() -> ValkeyResult { 28 | let output = vec![ 29 | ValkeyValue::SimpleString("cmd1 - top level command".into()), 30 | ValkeyValue::SimpleString("cmd1 s1 - first level subcommand".into()), 31 | ValkeyValue::SimpleString("cmd1 s1 s1 - second level command".into()), 32 | ValkeyValue::SimpleString("cmd1 s1 s1 s1 - third level command".into()), 33 | ValkeyValue::SimpleString("cmd1 help - display this message".into()), 34 | ]; 35 | Ok(output.into()) 36 | } 37 | 38 | // custom info subcommand, can be called with `cmd1 info` 39 | fn info_subcmd(args: Vec) -> ValkeyResult { 40 | let section = args.into_iter().next_str().unwrap_or("all"); 41 | 42 | let sections = [ 43 | ("key", ValkeyValue::SimpleString("value".into())), 44 | ("integer", ValkeyValue::Integer(1)), 45 | ("float", ValkeyValue::Float(1.1)), 46 | ("bool", ValkeyValue::Bool(true)), 47 | ( 48 | "array", 49 | ValkeyValue::Array(vec!["a", "b", "c"].into_iter().map(|s| s.into()).collect()), 50 | ), 51 | ( 52 | "ordered-map", 53 | ValkeyValue::OrderedMap(BTreeMap::from([ 54 | ("key1".into(), "value1".into()), 55 | ("key2".into(), "value2".into()), 56 | ])), 57 | ), 58 | ( 59 | "ordered-set", 60 | ValkeyValue::OrderedSet(vec!["x", "y", "z"].into_iter().map(|s| s.into()).collect()), 61 | ), 62 | // add more sections here as needed 63 | ]; 64 | 65 | // using BTreeMap to maintain order 66 | let mut output: BTreeMap = BTreeMap::new(); 67 | for (key, value) in sections { 68 | if section == "all" || section == key { 69 | output.insert(key.into(), value); 70 | } 71 | } 72 | Ok(ValkeyValue::OrderedMap(output)) 73 | } 74 | fn sub1(args: Vec) -> ValkeyResult { 75 | if args.len() == 0 { 76 | // return if no args for subcmd are passed in, additional bizlogic can be added here 77 | return Ok("sub1".into()); 78 | }; 79 | let mut args = args.into_iter(); 80 | let subcmd = args.next_string()?; 81 | let args: Vec = args.collect(); 82 | match subcmd.to_lowercase().as_str() { 83 | "s1" => sub11(args), 84 | // more subcommands can be added here 85 | _ => Ok("sub1".into()), 86 | } 87 | } 88 | 89 | fn sub11(args: Vec) -> ValkeyResult { 90 | if args.len() == 0 { 91 | // return if no args for subcmd are passed in, additional bizlogic can be added here 92 | return Ok("sub11".into()); 93 | }; 94 | let mut args = args.into_iter(); 95 | let subcmd = args.next_string()?; 96 | let args: Vec = args.collect(); 97 | match subcmd.to_lowercase().as_str() { 98 | "s1" => sub111(args), 99 | // more subcommands can be added here 100 | _ => Ok("sub11".into()), 101 | } 102 | } 103 | 104 | fn sub111(_args: Vec) -> ValkeyResult { 105 | // add bizlogic here 106 | Ok("sub111".into()) 107 | } 108 | 109 | ////////////////////////////////////////////////////// 110 | 111 | valkey_module! { 112 | name: "subcmd", 113 | version: 1, 114 | allocator: (ValkeyAlloc, ValkeyAlloc), 115 | data_types: [], 116 | commands: [ 117 | ["cmd1", cmd1, "", 0, 0, 0], 118 | ], 119 | } 120 | -------------------------------------------------------------------------------- /examples/crontab.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::str::FromStr; 3 | use std::sync::{LazyLock, RwLock}; 4 | use valkey_module::alloc::ValkeyAlloc; 5 | use valkey_module::{valkey_module, Context, Status, ValkeyString}; 6 | use valkey_module_macros::cron_event_handler; 7 | 8 | // struct to hold environment-specific configs, based on the environment name passed in via MODULE LOAD 9 | #[derive(Debug)] 10 | struct EnvConfig { 11 | cron_fn1_fn2: String, 12 | cron_fn3: String, 13 | // add more environment-specific configs here 14 | } 15 | impl EnvConfig { 16 | pub(crate) fn new(env: &str) -> Self { 17 | let output = match env { 18 | "dev" => EnvConfig { 19 | // 5 and 10 seconds in dev 20 | cron_fn1_fn2: "*/5 * * * * * *".to_string(), 21 | cron_fn3: "*/10 * * * * * *".to_string(), 22 | }, 23 | // more environments can be added here 24 | _ => EnvConfig { 25 | // 15 and 30 seconds by default 26 | cron_fn1_fn2: "*/15 * * * * * *".to_string(), 27 | cron_fn3: "*/30 * * * * * *".to_string(), 28 | }, 29 | }; 30 | output 31 | } 32 | } 33 | // wrapper for EnvConfig 34 | static ENV_CONFIG: LazyLock> = LazyLock::new(|| RwLock::new(EnvConfig::new(""))); 35 | 36 | static CRONTAB: LazyLock>> = LazyLock::new(|| { 37 | // access the ENV_CONFIG to get cron expressions for the environment 38 | let env_config = ENV_CONFIG.read().unwrap(); 39 | let mut output = HashMap::new(); 40 | // map of cron expressions and their corresponding functions 41 | // using vector allows to run multiple functions at the same interval 42 | output.insert( 43 | env_config.cron_fn1_fn2.clone(), 44 | vec![cron_fn1 as fn(&Context), cron_fn2 as fn(&Context)], 45 | ); 46 | // every 30 seconds 47 | output.insert(env_config.cron_fn3.clone(), vec![cron_fn3 as fn(&Context)]); 48 | output 49 | }); 50 | 51 | fn cron_fn1(_ctx: &Context) { 52 | // biz logic here 53 | } 54 | 55 | fn cron_fn2(_ctx: &Context) { 56 | // biz logic here 57 | } 58 | 59 | fn cron_fn3(_ctx: &Context) { 60 | // biz logic here 61 | } 62 | 63 | // uses serverCron to execute custom code on schedule 64 | #[cron_event_handler] 65 | fn cron_event_handler(ctx: &Context, _hz: u64) { 66 | // default hz value is 10 but check what it's currently set 67 | // read valkey.conf for details 68 | let hz = match ctx.config_get("hz".to_string()) { 69 | Ok(tmp) => tmp.to_string().parse::().unwrap_or(10), 70 | Err(_) => 10, // default to 10 if config is not set or invalid 71 | }; 72 | // how many milliseconds between cron events 73 | let interval = 1000 / hz as i64; 74 | for (expression, functions) in CRONTAB.iter() { 75 | // explicitly use unwrap to crash if there are any issues with the cron expression 76 | let schedule = cron::Schedule::from_str(expression).unwrap(); 77 | let next_time = schedule.upcoming(chrono::Utc).next().unwrap_or_default(); 78 | let now = chrono::Utc::now(); 79 | // check if the next time is within the interval 80 | if next_time.timestamp_millis() <= now.timestamp_millis() + interval { 81 | // loop through functions for that interval 82 | for function in functions { 83 | function(ctx); 84 | } 85 | } 86 | } 87 | } 88 | 89 | fn initialize(ctx: &Context, args: &[ValkeyString]) -> Status { 90 | // if arg passed in MODULE LOAD use it to set env_name 91 | let env_name = match args.get(0) { 92 | Some(tmp) => tmp.to_string(), 93 | None => "".to_string(), 94 | }; 95 | // update ENV_CONFIG static variable based on the env_name 96 | let mut guard = ENV_CONFIG.write().unwrap(); 97 | *guard = EnvConfig::new(env_name.as_str()); 98 | drop(guard); 99 | // env_name, "dev", ENV_CONFIG: LazyLock(RwLock { data: EnvConfig { cron_fn1_fn2: "*/15 * * * * * *", cron_fn3: "*/30 * * * * * *" }, poisoned: false, .. }) 100 | ctx.log_notice(&format!( 101 | "env_name, {:?}, ENV_CONFIG: {:?}", 102 | env_name, ENV_CONFIG 103 | )); 104 | Status::Ok 105 | } 106 | 107 | valkey_module! { 108 | name: "crontab", 109 | version: 1, 110 | allocator: (ValkeyAlloc, ValkeyAlloc), 111 | data_types: [], 112 | init: initialize, 113 | commands: [ 114 | ], 115 | } 116 | -------------------------------------------------------------------------------- /examples/proc_macro_commands.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; 2 | 3 | use valkey_module::alloc::ValkeyAlloc; 4 | use valkey_module::ValkeyError; 5 | use valkey_module::{valkey_module, Context, ValkeyResult, ValkeyString, ValkeyValue}; 6 | use valkey_module_macros::{command, ValkeyValue}; 7 | 8 | #[derive(ValkeyValue)] 9 | struct ValkeyValueDeriveInner { 10 | i1: i64, 11 | } 12 | 13 | #[derive(ValkeyValue)] 14 | struct ValkeyValueDerive { 15 | i: i64, 16 | f: f64, 17 | s: String, 18 | u: usize, 19 | v: Vec, 20 | #[ValkeyValueAttr{flatten: true}] 21 | inner: ValkeyValueDeriveInner, 22 | v2: Vec, 23 | hash_map: HashMap, 24 | hash_set: HashSet, 25 | ordered_map: BTreeMap, 26 | ordered_set: BTreeSet, 27 | } 28 | 29 | #[derive(ValkeyValue)] 30 | enum ValkeyValueEnum { 31 | Str(String), 32 | ValkeyValue(ValkeyValueDerive), 33 | } 34 | 35 | #[command( 36 | { 37 | flags: [ReadOnly, NoMandatoryKeys], 38 | arity: -1, 39 | key_spec: [ 40 | { 41 | notes: "test valkey value derive macro", 42 | flags: [ReadOnly, Access], 43 | begin_search: Index({ index : 0 }), 44 | find_keys: Range({ last_key : 0, steps : 0, limit : 0 }), 45 | } 46 | ] 47 | } 48 | )] 49 | fn valkey_value_derive( 50 | _ctx: &Context, 51 | args: Vec, 52 | ) -> Result { 53 | if args.len() > 1 { 54 | Ok(ValkeyValueEnum::Str("OK".to_owned())) 55 | } else { 56 | Ok(ValkeyValueEnum::ValkeyValue(ValkeyValueDerive { 57 | i: 10, 58 | f: 1.1, 59 | s: "s".to_owned(), 60 | u: 20, 61 | v: vec![1, 2, 3], 62 | inner: ValkeyValueDeriveInner { i1: 1 }, 63 | v2: vec![ 64 | ValkeyValueDeriveInner { i1: 1 }, 65 | ValkeyValueDeriveInner { i1: 2 }, 66 | ], 67 | hash_map: HashMap::from([("key".to_owned(), "val".to_owned())]), 68 | hash_set: HashSet::from(["key".to_owned()]), 69 | ordered_map: BTreeMap::from([("key".to_owned(), ValkeyValueDeriveInner { i1: 10 })]), 70 | ordered_set: BTreeSet::from(["key".to_owned()]), 71 | })) 72 | } 73 | } 74 | 75 | #[command( 76 | { 77 | flags: [ReadOnly], 78 | arity: -2, 79 | key_spec: [ 80 | { 81 | notes: "test command that define all the arguments at even possition as keys", 82 | flags: [ReadOnly, Access], 83 | begin_search: Index({ index : 1 }), 84 | find_keys: Range({ last_key :- 1, steps : 2, limit : 0 }), 85 | } 86 | ] 87 | } 88 | )] 89 | fn classic_keys(_ctx: &Context, _args: Vec) -> ValkeyResult { 90 | Ok(ValkeyValue::SimpleStringStatic("OK")) 91 | } 92 | 93 | #[command( 94 | { 95 | name: "keyword_keys", 96 | flags: [ReadOnly], 97 | arity: -2, 98 | key_spec: [ 99 | { 100 | notes: "test command that define all the arguments at even possition as keys", 101 | flags: [ReadOnly, Access], 102 | begin_search: Keyword({ keyword : "foo", startfrom : 1 }), 103 | find_keys: Range({ last_key :- 1, steps : 2, limit : 0 }), 104 | } 105 | ] 106 | } 107 | )] 108 | fn keyword_keys(_ctx: &Context, _args: Vec) -> ValkeyResult { 109 | Ok(ValkeyValue::SimpleStringStatic("OK")) 110 | } 111 | 112 | #[command( 113 | { 114 | name: "num_keys", 115 | flags: [ReadOnly, NoMandatoryKeys], 116 | arity: -2, 117 | key_spec: [ 118 | { 119 | notes: "test command that define all the arguments at even possition as keys", 120 | flags: [ReadOnly, Access], 121 | begin_search: Index({ index : 1 }), 122 | find_keys: Keynum({ key_num_idx : 0, first_key : 1, key_step : 1 }), 123 | } 124 | ] 125 | } 126 | )] 127 | fn num_keys(_ctx: &Context, _args: Vec) -> ValkeyResult { 128 | Ok(ValkeyValue::SimpleStringStatic("OK")) 129 | } 130 | 131 | valkey_module! { 132 | name: "server_events", 133 | version: 1, 134 | allocator: (ValkeyAlloc, ValkeyAlloc), 135 | data_types: [], 136 | commands: [], 137 | } 138 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use crate::context::filter::{CommandFilter, CommandFilterCtx}; 2 | pub use crate::context::InfoContext; 3 | extern crate num_traits; 4 | 5 | pub mod alloc; 6 | pub mod apierror; 7 | pub mod defrag; 8 | pub mod digest; 9 | pub mod error; 10 | pub mod native_types; 11 | pub mod raw; 12 | pub mod rediserror; 13 | mod redismodule; 14 | pub mod redisraw; 15 | pub mod redisvalue; 16 | pub mod stream; 17 | 18 | pub mod configuration; 19 | mod context; 20 | pub mod key; 21 | pub mod logging; 22 | mod macros; 23 | mod utils; 24 | 25 | pub use crate::context::blocked::BlockedClient; 26 | pub use crate::context::thread_safe::{ 27 | ContextGuard, DetachedFromClient, ThreadSafeContext, ValkeyGILGuard, ValkeyLockIndicator, 28 | }; 29 | pub use crate::raw::NotifyEvent; 30 | 31 | pub use crate::configuration::ConfigurationValue; 32 | pub use crate::configuration::EnumConfigurationValue; 33 | pub use crate::context::call_reply::FutureCallReply; 34 | pub use crate::context::call_reply::{CallReply, CallResult, ErrorReply, PromiseCallReply}; 35 | pub use crate::context::commands; 36 | pub use crate::context::keys_cursor::KeysCursor; 37 | pub use crate::context::server_events; 38 | pub use crate::context::AclPermissions; 39 | #[cfg(all(any( 40 | feature = "min-valkey-compatibility-version-8-0", 41 | feature = "min-redis-compatibility-version-7-2" 42 | )))] 43 | pub use crate::context::BlockingCallOptions; 44 | pub use crate::context::CallOptionResp; 45 | pub use crate::context::CallOptions; 46 | pub use crate::context::CallOptionsBuilder; 47 | pub use crate::context::Context; 48 | pub use crate::context::ContextFlags; 49 | pub use crate::context::DetachedContext; 50 | pub use crate::context::DetachedContextGuard; 51 | pub use crate::context::{ 52 | InfoContextBuilderFieldBottomLevelValue, InfoContextBuilderFieldTopLevelValue, 53 | InfoContextFieldBottomLevelData, InfoContextFieldTopLevelData, OneInfoSectionData, 54 | }; 55 | pub use crate::raw::*; 56 | pub use crate::redismodule::*; 57 | use backtrace::Backtrace; 58 | use context::server_events::INFO_COMMAND_HANDLER_LIST; 59 | 60 | /// The detached Valkey module context (the context of this module). It 61 | /// is only set to a proper value after the module is initialised via the 62 | /// provided [redis_module] macro. 63 | /// See [DetachedContext]. 64 | pub static MODULE_CONTEXT: DetachedContext = DetachedContext::new(); 65 | 66 | #[deprecated( 67 | since = "2.1.0", 68 | note = "Please use the valkey_module::logging::ValkeyLogLevel directly instead." 69 | )] 70 | pub type LogLevel = logging::ValkeyLogLevel; 71 | 72 | fn add_trace_info(ctx: &InfoContext) -> ValkeyResult<()> { 73 | const SECTION_NAME: &str = "trace"; 74 | const FIELD_NAME: &str = "backtrace"; 75 | 76 | let current_backtrace = Backtrace::new(); 77 | let trace = format!("{current_backtrace:?}"); 78 | 79 | ctx.builder() 80 | .add_section(SECTION_NAME) 81 | .field(FIELD_NAME, trace)? 82 | .build_section()? 83 | .build_info()?; 84 | 85 | Ok(()) 86 | } 87 | 88 | /// A type alias for the custom info command handler. 89 | /// The function may optionally return an object of one section to add. 90 | /// If nothing is returned, it is assumed that the function has already 91 | /// filled all the information required via [`InfoContext::builder`]. 92 | pub type InfoHandlerFunctionType = fn(&InfoContext, bool) -> ValkeyResult<()>; 93 | 94 | /// Default "INFO" command handler for the module. 95 | /// 96 | /// This function can be invoked, for example, by sending `INFO modules` 97 | /// through the RESP protocol. 98 | pub fn basic_info_command_handler(ctx: &InfoContext, for_crash_report: bool) { 99 | if for_crash_report { 100 | if let Err(e) = add_trace_info(ctx) { 101 | log::error!("Couldn't send info for the module: {e}"); 102 | return; 103 | } 104 | } 105 | 106 | INFO_COMMAND_HANDLER_LIST 107 | .iter() 108 | .filter_map(|callback| callback(ctx, for_crash_report).err()) 109 | .for_each(|e| log::error!("Couldn't build info for the module's custom handler: {e}")); 110 | } 111 | 112 | /// Initialize RedisModuleAPI or ValkeyModuleAPI without register as a module. 113 | pub fn init_api(ctx: &Context) { 114 | if use_redis_module_api() { 115 | unsafe { Export_RedisModule_InitAPI(ctx.ctx) }; 116 | } else { 117 | unsafe { Export_ValkeyModule_InitAPI(ctx.ctx as *mut raw::ValkeyModuleCtx) }; 118 | } 119 | } 120 | 121 | pub(crate) unsafe fn deallocate_pointer

(p: *mut P) { 122 | std::ptr::drop_in_place(p); 123 | std::alloc::dealloc(p as *mut u8, std::alloc::Layout::new::

()); 124 | } 125 | -------------------------------------------------------------------------------- /examples/data_type.rs: -------------------------------------------------------------------------------- 1 | use std::os::raw::c_void; 2 | use valkey_module::alloc::ValkeyAlloc; 3 | use valkey_module::defrag::Defrag; 4 | use valkey_module::digest::Digest; 5 | use valkey_module::native_types::ValkeyType; 6 | use valkey_module::{ 7 | raw, valkey_module, Context, NextArg, RedisModuleString, ValkeyResult, ValkeyString, 8 | }; 9 | 10 | #[derive(Debug)] 11 | struct MyType { 12 | data: String, 13 | } 14 | 15 | static MY_VALKEY_TYPE: ValkeyType = ValkeyType::new( 16 | "mytype123", 17 | 0, 18 | raw::RedisModuleTypeMethods { 19 | version: raw::REDISMODULE_TYPE_METHOD_VERSION as u64, 20 | rdb_load: None, 21 | rdb_save: None, 22 | aof_rewrite: None, 23 | free: Some(free), 24 | digest: Some(digest), 25 | mem_usage: None, 26 | 27 | // Aux data 28 | aux_load: None, 29 | aux_save: None, 30 | aux_save2: None, 31 | aux_save_triggers: 0, 32 | 33 | free_effort: None, 34 | unlink: None, 35 | copy: None, 36 | defrag: Some(defrag), 37 | 38 | copy2: None, 39 | free_effort2: None, 40 | mem_usage2: None, 41 | unlink2: None, 42 | }, 43 | ); 44 | 45 | unsafe extern "C" fn free(value: *mut c_void) { 46 | drop(Box::from_raw(value.cast::())); 47 | } 48 | 49 | unsafe extern "C" fn digest(md: *mut raw::RedisModuleDigest, value: *mut c_void) { 50 | let mut dig = Digest::new(md); 51 | let val = &*(value.cast::()); 52 | dig.add_string_buffer(&val.data.as_bytes()); 53 | dig.end_sequence(); 54 | } 55 | 56 | unsafe extern "C" fn defrag( 57 | defrag_ctx: *mut raw::RedisModuleDefragCtx, 58 | _from_key: *mut RedisModuleString, 59 | value: *mut *mut c_void, 60 | ) -> i32 { 61 | let defrag = Defrag::new(defrag_ctx); 62 | let ptr_ret = defrag.alloc(*value); 63 | if !ptr_ret.is_null() { 64 | *value = ptr_ret; 65 | } 66 | // Example usage of how shouldstopdefrag and defrag cursors would work. The data type used in this example is not complicated enough to use defrag cursors and 67 | // 'should_stop_defrag' so this is just used to show how it could be used for a more compicated datatype. 68 | let mut cursor = defrag.get_cursor().unwrap_or(0); 69 | // The call below will return the db_id of the current item we are defragging 70 | let _db_id = defrag.get_db_id_from_defrag_context(); 71 | // The call below will return the key name of the item we are defragging 72 | let _curr_key_name = defrag.get_key_name_from_defrag_context(); 73 | let number_of_allocations_in_our_data_type = 100; 74 | while cursor < number_of_allocations_in_our_data_type && !defrag.should_stop_defrag() { 75 | // Perform some defrag action i.e call defrag.alloc on the inner mechanism of the data type 76 | cursor += 1; 77 | } 78 | // If not all filters were looked at, return 1 to indicate incomplete defragmentation 79 | if cursor < number_of_allocations_in_our_data_type { 80 | // Save the cursor for where we will start defragmenting from next time 81 | defrag.set_cursor(cursor); 82 | return 1; 83 | } 84 | // 85 | 0 86 | } 87 | 88 | fn alloc_set(ctx: &Context, args: Vec) -> ValkeyResult { 89 | let mut args = args.into_iter().skip(1); 90 | let key = args.next_arg()?; 91 | let size = args.next_i64()?; 92 | 93 | ctx.log_debug(format!("key: {key}, size: {size}").as_str()); 94 | 95 | let key = ctx.open_key_writable(&key); 96 | 97 | if let Some(value) = key.get_value::(&MY_VALKEY_TYPE)? { 98 | value.data = "B".repeat(size as usize); 99 | } else { 100 | let value = MyType { 101 | data: "A".repeat(size as usize), 102 | }; 103 | 104 | key.set_value(&MY_VALKEY_TYPE, value)?; 105 | } 106 | Ok(size.into()) 107 | } 108 | 109 | fn alloc_get(ctx: &Context, args: Vec) -> ValkeyResult { 110 | let mut args = args.into_iter().skip(1); 111 | let key = args.next_arg()?; 112 | 113 | let key = ctx.open_key(&key); 114 | 115 | let value = match key.get_value::(&MY_VALKEY_TYPE)? { 116 | Some(value) => value.data.as_str().into(), 117 | None => ().into(), 118 | }; 119 | 120 | Ok(value) 121 | } 122 | 123 | ////////////////////////////////////////////////////// 124 | 125 | valkey_module! { 126 | name: "alloc", 127 | version: 1, 128 | allocator: (ValkeyAlloc, ValkeyAlloc), 129 | data_types: [ 130 | MY_VALKEY_TYPE, 131 | ], 132 | commands: [ 133 | ["alloc.set", alloc_set, "write", 1, 1, 1], 134 | ["alloc.get", alloc_get, "readonly", 1, 1, 1], 135 | ], 136 | } 137 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "valkey-module" 3 | version = "0.1.10" 4 | authors = ["Dmitry Polyakovsky "] 5 | edition = "2021" 6 | build = "build.rs" 7 | description = "A toolkit for building valkey modules in Rust" 8 | license = "BSD-3-Clause" 9 | repository = "https://github.com/valkey-io/valkeymodule-rs" 10 | readme = "README.md" 11 | keywords = ["valkey", "plugin"] 12 | categories = ["database", "api-bindings"] 13 | 14 | [[example]] 15 | name = "hello" 16 | crate-type = ["cdylib"] 17 | 18 | [[example]] 19 | name = "string" 20 | crate-type = ["cdylib"] 21 | 22 | [[example]] 23 | name = "configuration" 24 | crate-type = ["cdylib"] 25 | 26 | [[example]] 27 | name = "proc_macro_commands" 28 | crate-type = ["cdylib"] 29 | 30 | [[example]] 31 | name = "acl" 32 | crate-type = ["cdylib"] 33 | required-features = ["min-valkey-compatibility-version-8-0"] 34 | 35 | [[example]] 36 | name = "auth" 37 | crate-type = ["cdylib"] 38 | required-features = ["min-redis-compatibility-version-7-2"] 39 | 40 | [[example]] 41 | name = "call" 42 | crate-type = ["cdylib"] 43 | required-features = ["min-redis-compatibility-version-7-2"] 44 | 45 | [[example]] 46 | name = "keys_pos" 47 | crate-type = ["cdylib"] 48 | 49 | [[example]] 50 | name = "lists" 51 | crate-type = ["cdylib"] 52 | 53 | [[example]] 54 | name = "timer" 55 | crate-type = ["cdylib"] 56 | 57 | [[example]] 58 | name = "threads" 59 | crate-type = ["cdylib"] 60 | 61 | [[example]] 62 | name = "block" 63 | crate-type = ["cdylib"] 64 | 65 | [[example]] 66 | name = "data_type" 67 | crate-type = ["cdylib"] 68 | 69 | [[example]] 70 | name = "data_type2" 71 | crate-type = ["cdylib"] 72 | 73 | [[example]] 74 | name = "data_type3" 75 | crate-type = ["cdylib"] 76 | 77 | [[example]] 78 | name = "load_unload" 79 | crate-type = ["cdylib"] 80 | 81 | [[example]] 82 | name = "ctx_flags" 83 | crate-type = ["cdylib"] 84 | 85 | [[example]] 86 | name = "server_events" 87 | crate-type = ["cdylib"] 88 | 89 | [[example]] 90 | name = "events" 91 | crate-type = ["cdylib"] 92 | 93 | [[example]] 94 | name = "test_helper" 95 | crate-type = ["cdylib"] 96 | 97 | [[example]] 98 | name = "info_handler_macro" 99 | crate-type = ["cdylib"] 100 | 101 | [[example]] 102 | name = "info_handler_builder" 103 | crate-type = ["cdylib"] 104 | 105 | [[example]] 106 | name = "info_handler_struct" 107 | crate-type = ["cdylib"] 108 | 109 | [[example]] 110 | name = "info_handler_multiple_sections" 111 | crate-type = ["cdylib"] 112 | 113 | [[example]] 114 | name = "info" 115 | crate-type = ["cdylib"] 116 | 117 | [[example]] 118 | name = "scan_keys" 119 | crate-type = ["cdylib"] 120 | 121 | [[example]] 122 | name = "stream" 123 | crate-type = ["cdylib"] 124 | 125 | [[example]] 126 | name = "response" 127 | crate-type = ["cdylib"] 128 | 129 | [[example]] 130 | name = "open_key_with_flags" 131 | crate-type = ["cdylib"] 132 | 133 | [[example]] 134 | name = "expire" 135 | crate-type = ["cdylib"] 136 | 137 | [[example]] 138 | name = "client" 139 | crate-type = ["cdylib"] 140 | 141 | [[example]] 142 | name = "filter1" 143 | crate-type = ["cdylib"] 144 | 145 | [[example]] 146 | name = "filter2" 147 | crate-type = ["cdylib"] 148 | 149 | [[example]] 150 | name = "preload" 151 | crate-type = ["cdylib"] 152 | 153 | [[example]] 154 | name = "subcmd" 155 | crate-type = ["cdylib"] 156 | 157 | [[example]] 158 | name = "crontab" 159 | crate-type = ["cdylib"] 160 | 161 | [dependencies] 162 | bitflags = "2.8.0" 163 | libc = "0.2" 164 | enum-primitive-derive = "^0.1" 165 | num-traits = "^0.2" 166 | regex = "1" 167 | strum_macros = "0.26" 168 | backtrace = "0.3" 169 | linkme = "0.3" 170 | serde = { version = "1", features = ["derive"] } 171 | nix = "0.26" 172 | cfg-if = "1" 173 | valkey-module-macros-internals = { path = "valkeymodule-rs-macros-internals", version = "0.1.4"} 174 | log = "0.4" 175 | paste = "1.0.15" 176 | 177 | [dev-dependencies] 178 | anyhow = "1" 179 | redis = "0.28" 180 | lazy_static = "1" 181 | valkey-module-macros = { path = "valkeymodule-rs-macros", version = "0.1.4" } 182 | valkey-module = { path = "./", default-features = false, features = ["min-valkey-compatibility-version-8-0", "min-redis-compatibility-version-7-2"] } 183 | cron = "0.15.0" 184 | chrono = "0.4.41" 185 | dashmap = "6.1.0" 186 | 187 | [build-dependencies] 188 | bindgen = "0.70" 189 | cc = "1" 190 | 191 | [features] 192 | default = ["min-redis-compatibility-version-7-0"] 193 | min-valkey-compatibility-version-8-0 = [] 194 | min-redis-compatibility-version-7-2 = [] 195 | min-redis-compatibility-version-7-0 = [] 196 | min-redis-compatibility-version-6-2 = [] 197 | min-redis-compatibility-version-6-0 = [] 198 | # this is used to enable System.alloc instead of ValkeyAlloc for tests 199 | enable-system-alloc = [] 200 | # this is to indicate the Module wants to use RedisModule APIs for calls 201 | use-redismodule-api = [] 202 | -------------------------------------------------------------------------------- /valkeymodule-rs-macros/src/info_section.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use proc_macro2::Ident; 3 | use quote::{quote, ToTokens}; 4 | use syn::{parse_macro_input, Data, DataStruct, DeriveInput, Fields}; 5 | 6 | /// Returns `true` if the `type_string` provided is a type of a map 7 | /// supported by the [`crate::InfoSection`]. 8 | pub fn is_supported_map(type_string: &str) -> bool { 9 | /// A list of supported maps which can be converted to a dictionary for 10 | /// the [`valkey_module::InfoContext`]. 11 | const ALL: [&str; 2] = ["BTreeMap", "HashMap"]; 12 | 13 | ALL.iter().any(|m| type_string.contains(&m.to_lowercase())) 14 | } 15 | 16 | /// Generate a [`From`] implementation for this struct so that it is 17 | /// possible to generate a [`valkey_module::InfoContext`] information 18 | /// from it. 19 | /// 20 | /// A struct is compatible to be used with [`crate::InfoSection`] when 21 | /// it has fields, whose types are convertible to 22 | /// [`valkey_module::InfoContextBuilderFieldTopLevelValue`] and (for 23 | /// the dictionaries) if it has fields which are compatible maps of 24 | /// objects, where a key is a [`String`] and a value is any type 25 | /// convertible to 26 | /// [`valkey_module::InfoContextBuilderFieldBottomLevelValue`]. 27 | fn struct_info_section(struct_name: Ident, struct_data: DataStruct) -> TokenStream { 28 | let fields = match struct_data.fields { 29 | Fields::Named(f) => f, 30 | _ => { 31 | return quote! {compile_error!("The InfoSection can only be derived for structs with named fields.")}.into() 32 | } 33 | }; 34 | 35 | let fields = fields 36 | .named 37 | .into_iter() 38 | .map(|v| { 39 | let is_dictionary = 40 | is_supported_map(&v.ty.clone().into_token_stream().to_string().to_lowercase()); 41 | let name = v.ident.ok_or( 42 | "Structs with unnamed fields are not supported by the InfoSection.".to_owned(), 43 | )?; 44 | Ok((is_dictionary, name)) 45 | }) 46 | .collect::, String>>(); 47 | 48 | let section_key_fields: Vec<_> = match fields { 49 | Ok(ref f) => f.iter().filter(|i| !i.0).map(|i| i.1.clone()).collect(), 50 | Err(e) => return quote! {compile_error!(#e)}.into(), 51 | }; 52 | 53 | let section_dictionary_fields: Vec<_> = match fields { 54 | Ok(f) => f.iter().filter(|i| i.0).map(|i| i.1.clone()).collect(), 55 | Err(e) => return quote! {compile_error!(#e)}.into(), 56 | }; 57 | 58 | let key_fields_names: Vec<_> = section_key_fields.iter().map(|v| v.to_string()).collect(); 59 | 60 | let dictionary_fields_names: Vec<_> = section_dictionary_fields 61 | .iter() 62 | .map(|v| v.to_string()) 63 | .collect(); 64 | 65 | quote! { 66 | impl From<#struct_name> for valkey_module::OneInfoSectionData { 67 | fn from(val: #struct_name) -> valkey_module::OneInfoSectionData { 68 | let section_name = stringify!(#struct_name).to_owned(); 69 | 70 | let fields = vec![ 71 | // The section's key => value pairs. 72 | #(( 73 | #key_fields_names.to_owned(), 74 | valkey_module::InfoContextBuilderFieldTopLevelValue::from(val.#section_key_fields) 75 | ), )* 76 | 77 | // The dictionaries within this section. 78 | #(( 79 | #dictionary_fields_names.to_owned(), 80 | valkey_module::InfoContextBuilderFieldTopLevelValue::Dictionary { 81 | name: #dictionary_fields_names.to_owned(), 82 | fields: valkey_module::InfoContextFieldBottomLevelData( 83 | val.#section_dictionary_fields 84 | .into_iter() 85 | .map(|d| d.into()) 86 | .collect()), 87 | } 88 | ), )* 89 | ]; 90 | (section_name, fields) 91 | } 92 | } 93 | } 94 | .into() 95 | } 96 | 97 | /// Implementation for the [`crate::info_section`] derive macro. 98 | /// Runs the relevant code generation base on the element 99 | /// the macro was used on. Currently supports `struct`s only. 100 | pub fn info_section(item: TokenStream) -> TokenStream { 101 | let input: DeriveInput = parse_macro_input!(item); 102 | let ident = input.ident; 103 | match input.data { 104 | Data::Struct(s) => struct_info_section(ident, s), 105 | _ => { 106 | quote! {compile_error!("The InfoSection derive can only be used with structs.")}.into() 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/context/timer.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | use std::ffi::c_void; 3 | use std::time::Duration; 4 | 5 | use crate::raw; 6 | use crate::raw::RedisModuleTimerID; 7 | use crate::{Context, ValkeyError}; 8 | 9 | // We use `repr(C)` since we access the underlying data field directly. 10 | // The order matters: the data field must come first. 11 | #[repr(C)] 12 | struct CallbackData { 13 | data: T, 14 | callback: F, 15 | } 16 | 17 | impl Context { 18 | /// Wrapper for `RedisModule_CreateTimer`. 19 | /// 20 | /// This function takes ownership of the provided data, and transfers it to Redis. 21 | /// The callback will get the original data back in a type safe manner. 22 | /// When the callback is done, the data will be dropped. 23 | pub fn create_timer( 24 | &self, 25 | period: Duration, 26 | callback: F, 27 | data: T, 28 | ) -> RedisModuleTimerID 29 | where 30 | F: FnOnce(&Self, T), 31 | { 32 | let cb_data = CallbackData { data, callback }; 33 | 34 | // Store the user-provided data on the heap before passing ownership of it to Redis, 35 | // so that it will outlive the current scope. 36 | let data = Box::from(cb_data); 37 | 38 | // Take ownership of the data inside the box and obtain a raw pointer to pass to Redis. 39 | let data = Box::into_raw(data); 40 | 41 | unsafe { 42 | raw::RedisModule_CreateTimer.unwrap()( 43 | self.ctx, 44 | period 45 | .as_millis() 46 | .try_into() 47 | .expect("Value must fit in 64 bits"), 48 | Some(raw_callback::), 49 | data.cast::(), 50 | ) 51 | } 52 | } 53 | 54 | /// Wrapper for `RedisModule_StopTimer`. 55 | /// 56 | /// The caller is responsible for specifying the correct type for the returned data. 57 | /// This function has no way to know what the original type of the data was, so the 58 | /// same data type that was used for `create_timer` needs to be passed here to ensure 59 | /// their types are identical. 60 | pub fn stop_timer(&self, timer_id: RedisModuleTimerID) -> Result { 61 | let mut data: *mut c_void = std::ptr::null_mut(); 62 | 63 | let status: raw::Status = 64 | unsafe { raw::RedisModule_StopTimer.unwrap()(self.ctx, timer_id, &mut data) }.into(); 65 | 66 | if status != raw::Status::Ok { 67 | return Err(ValkeyError::Str( 68 | "RedisModule_StopTimer failed, timer may not exist", 69 | )); 70 | } 71 | 72 | let data: T = take_data(data); 73 | Ok(data) 74 | } 75 | 76 | /// Wrapper for `RedisModule_GetTimerInfo`. 77 | /// 78 | /// The caller is responsible for specifying the correct type for the returned data. 79 | /// This function has no way to know what the original type of the data was, so the 80 | /// same data type that was used for `create_timer` needs to be passed here to ensure 81 | /// their types are identical. 82 | pub fn get_timer_info( 83 | &self, 84 | timer_id: RedisModuleTimerID, 85 | ) -> Result<(Duration, &T), ValkeyError> { 86 | let mut remaining: u64 = 0; 87 | let mut data: *mut c_void = std::ptr::null_mut(); 88 | 89 | let status: raw::Status = unsafe { 90 | raw::RedisModule_GetTimerInfo.unwrap()(self.ctx, timer_id, &mut remaining, &mut data) 91 | } 92 | .into(); 93 | 94 | if status != raw::Status::Ok { 95 | return Err(ValkeyError::Str( 96 | "RedisModule_GetTimerInfo failed, timer may not exist", 97 | )); 98 | } 99 | 100 | // Cast the *mut c_void supplied by the Valkey API to a raw pointer of our custom type. 101 | let data = data.cast::(); 102 | 103 | // Dereference the raw pointer (we know this is safe, since Valkey should return our 104 | // original pointer which we know to be good) and turn it into a safe reference 105 | let data = unsafe { &*data }; 106 | 107 | Ok((Duration::from_millis(remaining), data)) 108 | } 109 | } 110 | 111 | fn take_data(data: *mut c_void) -> T { 112 | // Cast the *mut c_void supplied by the Valkey API to a raw pointer of our custom type. 113 | let data = data.cast::(); 114 | 115 | // Take back ownership of the original boxed data, so we can unbox it safely. 116 | // If we don't do this, the data's memory will be leaked. 117 | let data = unsafe { Box::from_raw(data) }; 118 | 119 | *data 120 | } 121 | 122 | extern "C" fn raw_callback(ctx: *mut raw::RedisModuleCtx, data: *mut c_void) 123 | where 124 | F: FnOnce(&Context, T), 125 | { 126 | let ctx = &Context::new(ctx); 127 | 128 | if data.is_null() { 129 | ctx.log_debug("[callback] Data is null; this should not happen!"); 130 | return; 131 | } 132 | 133 | let cb_data: CallbackData = take_data(data); 134 | (cb_data.callback)(ctx, cb_data.data); 135 | } 136 | -------------------------------------------------------------------------------- /examples/client.rs: -------------------------------------------------------------------------------- 1 | use valkey_module::alloc::ValkeyAlloc; 2 | use valkey_module::{ 3 | valkey_module, Context, NextArg, Status, ValkeyError, ValkeyResult, ValkeyString, ValkeyValue, 4 | }; 5 | 6 | fn get_client_id(ctx: &Context, _args: Vec) -> ValkeyResult { 7 | let client_id = ctx.get_client_id(); 8 | Ok((client_id as i64).into()) 9 | } 10 | 11 | fn get_client_name(ctx: &Context, _args: Vec) -> ValkeyResult { 12 | // test for invalid client_id 13 | match ctx.get_client_name_by_id(0) { 14 | Ok(tmp) => ctx.log_notice(&format!( 15 | "client_id 0 client_name_by_id: {:?}", 16 | tmp.to_string() 17 | )), 18 | Err(err) => ctx.log_notice(&format!("client_id 0 client_name_by_id: {:?}", err)), 19 | } 20 | let client_name = ctx.get_client_name()?; 21 | Ok(ValkeyValue::from(client_name.to_string())) 22 | } 23 | 24 | fn get_client_username(ctx: &Context, _args: Vec) -> ValkeyResult { 25 | // test for invalid client_id 26 | match ctx.get_client_username_by_id(0) { 27 | Ok(tmp) => ctx.log_notice(&format!( 28 | "client_id 0 client_username_by_id: {:?}", 29 | tmp.to_string() 30 | )), 31 | Err(err) => ctx.log_notice(&format!("client_id 0 client_username_by_id: {:?}", err)), 32 | } 33 | let client_username = ctx.get_client_username()?; 34 | Ok(ValkeyValue::from(client_username.to_string())) 35 | } 36 | 37 | fn set_client_name(ctx: &Context, args: Vec) -> ValkeyResult { 38 | if args.len() != 2 { 39 | return Err(ValkeyError::WrongArity); 40 | } 41 | let mut args = args.into_iter().skip(1); 42 | let client_name = args.next_arg()?; 43 | // test for invalid client_id 44 | let resp1 = ctx.set_client_name_by_id(0, &client_name); 45 | ctx.log_notice(&format!("client_id 0 set_client_name_by_id: {:?}", resp1)); 46 | let resp2 = ctx.set_client_name(&client_name); 47 | Ok(ValkeyValue::Integer(resp2 as i64)) 48 | } 49 | 50 | fn get_client_cert(ctx: &Context, _args: Vec) -> ValkeyResult { 51 | // unless connection is made with cert, this will return Err, so just log it and return nothing 52 | match ctx.get_client_cert() { 53 | Ok(tmp) => ctx.log_notice(&format!("client_cert: {:?}", tmp.to_string())), 54 | Err(err) => ctx.log_notice(&format!("client_cert: {:?}", err.to_string())), 55 | } 56 | Ok("".into()) 57 | } 58 | 59 | fn get_client_info(ctx: &Context, _args: Vec) -> ValkeyResult { 60 | // test for invalid client_id 61 | let client_info_by_id = ctx.get_client_info_by_id(0); 62 | ctx.log_notice(&format!( 63 | "client_id 0 client_info_by_id: {:?}", 64 | client_info_by_id 65 | )); 66 | let client_info = ctx.get_client_info()?; 67 | ctx.log_notice(&format!("client_info: {:?}", client_info)); 68 | // return version like this: 69 | Ok(ValkeyValue::from(client_info.version.to_string())) 70 | } 71 | 72 | fn get_client_ip(ctx: &Context, _args: Vec) -> ValkeyResult { 73 | // test for invalid client_id 74 | let client_ip_by_id = ctx.get_client_ip_by_id(0); 75 | ctx.log_notice(&format!( 76 | "client_id 0 client_ip_by_id: {:?}", 77 | client_ip_by_id 78 | )); 79 | Ok(ctx.get_client_ip()?.into()) 80 | } 81 | 82 | fn deauth_client_by_id(ctx: &Context, args: Vec) -> ValkeyResult { 83 | if args.len() != 2 { 84 | return Err(ValkeyError::WrongArity); 85 | } 86 | let mut args = args.into_iter().skip(1); 87 | let client_id_str: ValkeyString = args.next_arg()?; 88 | let client_id: u64 = client_id_str.parse_integer()?.try_into()?; 89 | let resp = ctx.deauthenticate_and_close_client_by_id(client_id); 90 | match resp { 91 | Status::Ok => Ok(ValkeyValue::from("OK")), 92 | Status::Err => Err(ValkeyError::Str( 93 | "Failed to deauthenticate and close client", 94 | )), 95 | } 96 | } 97 | 98 | fn config_get(ctx: &Context, args: Vec) -> ValkeyResult { 99 | if args.len() != 2 { 100 | return Err(ValkeyError::WrongArity); 101 | } 102 | let mut args = args.into_iter().skip(1); 103 | let config_name: ValkeyString = args.next_arg()?; 104 | let config_value = ctx.config_get(config_name.to_string()); 105 | match config_value { 106 | Ok(value) => Ok(ValkeyValue::from(value.to_string())), 107 | Err(err) => Err(err), 108 | } 109 | } 110 | 111 | valkey_module! { 112 | name: "client", 113 | version: 1, 114 | allocator: (ValkeyAlloc, ValkeyAlloc), 115 | data_types: [], 116 | commands: [ 117 | ["client.id", get_client_id, "", 0, 0, 0], 118 | ["client.get_name", get_client_name, "", 0, 0, 0], 119 | ["client.set_name", set_client_name, "", 0, 0, 0], 120 | ["client.username", get_client_username, "", 0, 0, 0], 121 | ["client.cert", get_client_cert, "", 0, 0, 0], 122 | ["client.info", get_client_info, "", 0, 0, 0], 123 | ["client.ip", get_client_ip, "", 0, 0, 0], 124 | ["client.deauth", deauth_client_by_id, "", 0, 0, 0], 125 | ["client.config_get", config_get, "", 0, 0, 0] 126 | ] 127 | } 128 | -------------------------------------------------------------------------------- /src/context/filter.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | Context, RedisModuleCommandFilter, RedisModuleCommandFilterCtx, RedisModuleString, 3 | RedisModule_CommandFilterArgDelete, RedisModule_CommandFilterArgGet, 4 | RedisModule_CommandFilterArgInsert, RedisModule_CommandFilterArgReplace, 5 | RedisModule_CommandFilterArgsCount, RedisModule_CommandFilterGetClientId, 6 | RedisModule_RegisterCommandFilter, RedisModule_UnregisterCommandFilter, ValkeyString, 7 | }; 8 | use std::ffi::c_int; 9 | use std::str::Utf8Error; 10 | 11 | #[derive(Debug, Clone, Copy)] 12 | pub struct CommandFilter { 13 | inner: *mut RedisModuleCommandFilter, 14 | } 15 | 16 | // otherwise you get error: cannot be shared between threads safely 17 | unsafe impl Send for CommandFilter {} 18 | unsafe impl Sync for CommandFilter {} 19 | 20 | /// CommandFilter is a wrapper around the RedisModuleCommandFilter pointer 21 | /// 22 | /// It provides a way to check if the filter is null and to create a new CommandFilter 23 | impl CommandFilter { 24 | pub fn new(inner: *mut RedisModuleCommandFilter) -> Self { 25 | CommandFilter { inner } 26 | } 27 | 28 | pub fn is_null(&self) -> bool { 29 | self.inner.is_null() 30 | } 31 | } 32 | 33 | pub struct CommandFilterCtx { 34 | inner: *mut RedisModuleCommandFilterCtx, 35 | } 36 | 37 | /// wrapping the RedisModuleCommandFilterCtx to provide a higher level interface to call RedisModule_CommandFilter* 38 | /// 39 | /// provides functions to interact with the command filter context, such as getting the number of arguments, getting and replacing arguments, and deleting arguments. 40 | impl CommandFilterCtx { 41 | pub fn new(inner: *mut RedisModuleCommandFilterCtx) -> Self { 42 | CommandFilterCtx { inner } 43 | } 44 | 45 | /// wrapper for RedisModule_CommandFilterArgsCount 46 | pub fn args_count(&self) -> c_int { 47 | unsafe { RedisModule_CommandFilterArgsCount.unwrap()(self.inner) } 48 | } 49 | 50 | /// wrapper for RedisModule_CommandFilterArgGet 51 | pub fn arg_get(&self, pos: c_int) -> *mut RedisModuleString { 52 | unsafe { RedisModule_CommandFilterArgGet.unwrap()(self.inner, pos) } 53 | } 54 | 55 | /// wrapper to get argument as a Result<&str, Utf8Error> instead of RedisModuleString 56 | pub fn arg_get_try_as_str<'a>(&self, pos: c_int) -> Result<&'a str, Utf8Error> { 57 | let arg = self.arg_get(pos); 58 | ValkeyString::from_ptr(arg) 59 | } 60 | 61 | /// wrapper to get 0 argument, the command which is always present and return as &str 62 | pub fn cmd_get_try_as_str<'a>(&self) -> Result<&'a str, Utf8Error> { 63 | let cmd = self.arg_get(0); 64 | ValkeyString::from_ptr(cmd) 65 | } 66 | 67 | /// wrapper to get Vector of all args minus the command (0th arg) 68 | pub fn get_all_args_wo_cmd(&self) -> Vec<&str> { 69 | let mut output = Vec::new(); 70 | for pos in 1..self.args_count() { 71 | match self.arg_get_try_as_str(pos) { 72 | Ok(arg) => output.push(arg), 73 | Err(_) => continue, // skip invalid args 74 | } 75 | } 76 | output 77 | } 78 | 79 | /// wrapper for RedisModule_CommandFilterArgReplace, accepts simple &str and casts it to *mut RedisModuleString 80 | pub fn arg_replace(&self, pos: c_int, arg: &str) { 81 | unsafe { 82 | RedisModule_CommandFilterArgReplace.unwrap()( 83 | self.inner, 84 | pos, 85 | ValkeyString::create_and_retain(arg).inner, 86 | ) 87 | }; 88 | } 89 | 90 | /// wrapper for RedisModule_CommandFilterArgInsert, accepts simple &str and casts it to *mut RedisModuleString 91 | pub fn arg_insert(&self, pos: c_int, arg: &str) { 92 | unsafe { 93 | RedisModule_CommandFilterArgInsert.unwrap()( 94 | self.inner, 95 | pos, 96 | ValkeyString::create_and_retain(arg).inner, 97 | ) 98 | }; 99 | } 100 | 101 | /// wrapper for RedisModule_CommandFilterArgDelete 102 | pub fn arg_delete(&self, pos: c_int) { 103 | unsafe { RedisModule_CommandFilterArgDelete.unwrap()(self.inner, pos) }; 104 | } 105 | 106 | /// wrapper for RedisModule_CommandFilterGetClientId, not supported in Redis 7.0 107 | #[cfg(all(any( 108 | feature = "min-redis-compatibility-version-7-2", 109 | feature = "min-valkey-compatibility-version-8-0" 110 | ),))] 111 | pub fn get_client_id(&self) -> u64 { 112 | unsafe { RedisModule_CommandFilterGetClientId.unwrap()(self.inner) } 113 | } 114 | } 115 | 116 | /// adding functions to the Context struct to provide a higher level interface to register and unregister filters 117 | impl Context { 118 | /// wrapper for RedisModule_RegisterCommandFilter to directly register a filter, likely in init 119 | pub fn register_command_filter( 120 | &self, 121 | module_cmd_filter_func: extern "C" fn(*mut RedisModuleCommandFilterCtx), 122 | flags: u32, 123 | ) -> CommandFilter { 124 | let module_cmd_filter = unsafe { 125 | RedisModule_RegisterCommandFilter.unwrap()( 126 | self.ctx, 127 | Some(module_cmd_filter_func), 128 | flags as c_int, 129 | ) 130 | }; 131 | CommandFilter::new(module_cmd_filter) 132 | } 133 | 134 | /// wrapper for RedisModule_UnregisterCommandFilter to directly unregister filter, likely in deinit 135 | pub fn unregister_command_filter(&self, cmd_filter: &CommandFilter) { 136 | unsafe { 137 | RedisModule_UnregisterCommandFilter.unwrap()(self.ctx, cmd_filter.inner); 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /tests/utils.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | 3 | use redis::Connection; 4 | use redis::RedisResult; 5 | use std::fs; 6 | use std::path::PathBuf; 7 | use std::process::Command; 8 | use std::time::Duration; 9 | 10 | pub struct ChildGuard { 11 | name: &'static str, 12 | child: std::process::Child, 13 | } 14 | 15 | impl Drop for ChildGuard { 16 | fn drop(&mut self) { 17 | if let Err(e) = self.child.kill() { 18 | println!("Could not kill {}: {e}", self.name); 19 | } 20 | if let Err(e) = self.child.wait() { 21 | println!("Could not wait for {}: {e}", self.name); 22 | } 23 | } 24 | } 25 | 26 | pub fn start_valkey_server_with_module(module_name: &str, port: u16) -> Result { 27 | let module_path = get_module_path(module_name)?; 28 | 29 | let args = &[ 30 | "--port", 31 | &port.to_string(), 32 | "--loadmodule", 33 | module_path.as_str(), 34 | "--enable-debug-command", 35 | "yes", 36 | "--enable-module-command", 37 | "yes", 38 | ]; 39 | 40 | let valkey_server = Command::new("valkey-server") 41 | .args(args) 42 | .spawn() 43 | .map(|c| ChildGuard { 44 | name: "server", 45 | child: c, 46 | })?; 47 | 48 | Ok(valkey_server) 49 | } 50 | 51 | pub(crate) fn get_module_path(module_name: &str) -> Result { 52 | let extension = if cfg!(target_os = "macos") { 53 | "dylib" 54 | } else { 55 | "so" 56 | }; 57 | 58 | let profile = if cfg!(not(debug_assertions)) { 59 | "release" 60 | } else { 61 | "debug" 62 | }; 63 | 64 | let module_path: PathBuf = [ 65 | std::env::current_dir()?, 66 | PathBuf::from(format!( 67 | "target/{profile}/examples/lib{module_name}.{extension}" 68 | )), 69 | ] 70 | .iter() 71 | .collect(); 72 | 73 | assert!(fs::metadata(&module_path) 74 | .with_context(|| format!("Loading valkey module: {}", module_path.display()))? 75 | .is_file()); 76 | 77 | let module_path = format!("{}", module_path.display()); 78 | Ok(module_path) 79 | } 80 | 81 | // Get connection to Redis 82 | pub fn get_valkey_connection(port: u16) -> Result { 83 | let client = redis::Client::open(format!("redis://127.0.0.1:{port}/"))?; 84 | loop { 85 | let res = client.get_connection(); 86 | match res { 87 | Ok(con) => return Ok(con), 88 | Err(e) => { 89 | if e.is_connection_refusal() { 90 | // Valkey not ready yet, sleep and retry 91 | std::thread::sleep(Duration::from_millis(50)); 92 | } else { 93 | return Err(e.into()); 94 | } 95 | } 96 | } 97 | } 98 | } 99 | 100 | #[derive(Debug)] 101 | pub enum AuthExpectedResult { 102 | Success, 103 | Denied, 104 | EngineDenied, 105 | Aborted, 106 | } 107 | 108 | // Helper function to validate the authentication 109 | pub fn check_auth( 110 | con: &mut redis::Connection, 111 | username: &str, 112 | password: &str, 113 | expected_result: AuthExpectedResult, 114 | ) -> Result<()> { 115 | let response: RedisResult = redis::cmd("AUTH").arg(&[username, password]).query(con); 116 | 117 | match expected_result { 118 | AuthExpectedResult::Success => { 119 | let res = 120 | response.with_context(|| format!("failed to authenticate {username} user"))?; 121 | assert_eq!(res, "OK"); 122 | } 123 | AuthExpectedResult::Denied => { 124 | assert!(response.is_err()); 125 | let err = response.unwrap_err().to_string(); 126 | assert!( 127 | err.contains("DENIED: Authentication credentials mismatch"), 128 | "Unexpected error message: {}", 129 | err 130 | ); 131 | } 132 | AuthExpectedResult::EngineDenied => { 133 | assert!(response.is_err()); 134 | let err = response.unwrap_err().to_string(); 135 | assert!( 136 | err.contains("WRONGPASS: invalid username-password pair or user is disabled"), 137 | "Unexpected error message: {}", 138 | err 139 | ); 140 | } 141 | AuthExpectedResult::Aborted => { 142 | assert!(response.is_err()); 143 | let err = response.unwrap_err().to_string(); 144 | assert!( 145 | err.contains("ABORT: Authentication aborted by server"), 146 | "Unexpected error message: {}", 147 | err 148 | ); 149 | } 150 | } 151 | Ok(()) 152 | } 153 | 154 | pub fn setup_acl_users(con: &mut redis::Connection, users: &[(&str, Option<&str>)]) -> Result<()> { 155 | for (user, maybe_pass) in users { 156 | let res: String = if let Some(pass) = maybe_pass { 157 | redis::cmd("ACL") 158 | .arg(&["SETUSER", user, "on", &format!(">{}", pass), "~*", "+@all"]) 159 | .query(con)? 160 | } else { 161 | redis::cmd("ACL") 162 | .arg(&["SETUSER", user, "on", "nopass", "~*", "+@all"]) 163 | .query(con)? 164 | }; 165 | assert_eq!(&res, "OK"); 166 | } 167 | Ok(()) 168 | } 169 | 170 | pub fn check_blocked_clients(con: &mut redis::Connection) -> Result { 171 | let info: String = redis::cmd("INFO").arg("clients").query(con)?; 172 | 173 | let blocked_clients = info 174 | .lines() 175 | .find(|line| line.starts_with("blocked_clients:")) 176 | .and_then(|line| line.split(':').nth(1)) 177 | .and_then(|count| count.trim().parse::().ok()) 178 | .unwrap_or(0); 179 | 180 | Ok(blocked_clients) 181 | } 182 | -------------------------------------------------------------------------------- /valkeymodule-rs-macros-internals/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod api_versions; 2 | 3 | use api_versions::{get_feature_flags, API_OLDEST_VERSION, API_VERSION_MAPPING}; 4 | use proc_macro::TokenStream; 5 | use quote::quote; 6 | use syn::parse::{Parse, ParseStream, Result}; 7 | use syn::punctuated::Punctuated; 8 | use syn::token::{self, Paren, RArrow}; 9 | use syn::Ident; 10 | use syn::ItemFn; 11 | use syn::{self, bracketed, parse_macro_input, ReturnType, Token, Type, TypeTuple}; 12 | 13 | #[derive(Debug)] 14 | struct Args { 15 | requested_apis: Vec, 16 | function: ItemFn, 17 | } 18 | 19 | impl Parse for Args { 20 | fn parse(input: ParseStream) -> Result { 21 | let content; 22 | let _paren_token: token::Bracket = bracketed!(content in input); 23 | let vars: Punctuated = content.parse_terminated(Ident::parse)?; 24 | input.parse::()?; 25 | let function: ItemFn = input.parse()?; 26 | Ok(Args { 27 | requested_apis: vars.into_iter().collect(), 28 | function, 29 | }) 30 | } 31 | } 32 | 33 | /// This proc macro allows specifying which RedisModuleAPI is required by some valekymodue-rs 34 | /// function. The macro finds, for a given set of RedisModuleAPI, what the minimal Valkey version is 35 | /// that contains all those APIs and decides whether or not the function might raise an [APIError]. 36 | /// 37 | /// In addition, for each RedisModuleAPI, the proc macro injects a code that extracts the actual 38 | /// API function pointer and raises an error or panics if the API is invalid. 39 | /// 40 | /// # Panics 41 | /// 42 | /// Panics when an API is not available and if the function doesn't return [`Result`]. If it does 43 | /// return a [`Result`], the panics are replaced with returning a [`Result::Err`]. 44 | /// 45 | /// # Examples 46 | /// 47 | /// Creating a wrapper for the [`RedisModule_AddPostNotificationJob`] 48 | /// ```rust,no_run,ignore 49 | /// redismodule_api!( 50 | /// [RedisModule_AddPostNotificationJob], 51 | /// pub fn add_post_notification_job(&self, callback: F) -> Status { 52 | /// let callback = Box::into_raw(Box::new(callback)); 53 | /// unsafe { 54 | /// RedisModule_AddPostNotificationJob( 55 | /// self.ctx, 56 | /// Some(post_notification_job::), 57 | /// callback as *mut c_void, 58 | /// Some(post_notification_job_free_callback::), 59 | /// ) 60 | /// } 61 | /// .into() 62 | /// } 63 | /// ); 64 | /// ``` 65 | #[proc_macro] 66 | pub fn api(item: TokenStream) -> TokenStream { 67 | let args = parse_macro_input!(item as Args); 68 | let minimum_require_version = 69 | args.requested_apis 70 | .iter() 71 | .fold(*API_OLDEST_VERSION, |min_api_version, item| { 72 | // if we do not have a version mapping, we assume the API exists and return the minimum version. 73 | let api_version = API_VERSION_MAPPING 74 | .get(&item.to_string()) 75 | .map(|v| *v) 76 | .unwrap_or(*API_OLDEST_VERSION); 77 | api_version.max(min_api_version) 78 | }); 79 | 80 | let requested_apis = args.requested_apis; 81 | let requested_apis_str: Vec = requested_apis.iter().map(|e| e.to_string()).collect(); 82 | 83 | let original_func = args.function; 84 | let original_func_attr = original_func.attrs; 85 | let original_func_code = original_func.block; 86 | let original_func_sig = original_func.sig; 87 | let original_func_vis = original_func.vis; 88 | 89 | let inner_return_return_type = match original_func_sig.output.clone() { 90 | ReturnType::Default => Box::new(Type::Tuple(TypeTuple { 91 | paren_token: Paren::default(), 92 | elems: Punctuated::new(), 93 | })), 94 | ReturnType::Type(_, t) => t, 95 | }; 96 | let new_return_return_type = Type::Path( 97 | syn::parse( 98 | quote!( 99 | crate::apierror::APIResult<#inner_return_return_type> 100 | ) 101 | .into(), 102 | ) 103 | .unwrap(), 104 | ); 105 | 106 | let mut new_func_sig = original_func_sig.clone(); 107 | new_func_sig.output = ReturnType::Type(RArrow::default(), Box::new(new_return_return_type)); 108 | 109 | let old_ver_func = quote!( 110 | #(#original_func_attr)* 111 | #original_func_vis #new_func_sig { 112 | #( 113 | #[allow(non_snake_case)] 114 | let #requested_apis = unsafe{crate::raw::#requested_apis.ok_or(concat!(#requested_apis_str, " does not exists"))?}; 115 | )* 116 | let __callback__ = move || -> #inner_return_return_type { 117 | #original_func_code 118 | }; 119 | Ok(__callback__()) 120 | } 121 | ); 122 | 123 | let new_ver_func = quote!( 124 | #(#original_func_attr)* 125 | #original_func_vis #original_func_sig { 126 | #( 127 | #[allow(non_snake_case)] 128 | let #requested_apis = unsafe{crate::raw::#requested_apis.unwrap()}; 129 | )* 130 | let __callback__ = move || -> #inner_return_return_type { 131 | #original_func_code 132 | }; 133 | __callback__() 134 | } 135 | ); 136 | 137 | let (all_lower_features, all_upper_features) = get_feature_flags(minimum_require_version); 138 | 139 | let gen = quote! { 140 | cfg_if::cfg_if! { 141 | if #[cfg(any(#(#all_lower_features, )*))] { 142 | #old_ver_func 143 | } else if #[cfg(any(#(#all_upper_features, )*))] { 144 | #new_ver_func 145 | } else { 146 | compile_error!("min-redis-compatibility-version is not set correctly") 147 | } 148 | } 149 | }; 150 | gen.into() 151 | } 152 | -------------------------------------------------------------------------------- /valkeymodule-rs-macros/src/valkey_value.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use proc_macro2::Ident; 3 | use quote::quote; 4 | use serde::Deserialize; 5 | use serde_syn::{config, from_stream}; 6 | use syn::{ 7 | parse, 8 | parse::{Parse, ParseStream}, 9 | parse_macro_input, Data, DataEnum, DataStruct, DeriveInput, Fields, 10 | }; 11 | 12 | /// Generate [From] implementation for [ValkeyValue] for Enum. 13 | /// The generated code will simply check the Enum current type (using 14 | /// a match statement) and will perform [Into] and the matched variant. 15 | fn enum_valkey_value(struct_name: Ident, enum_data: DataEnum) -> TokenStream { 16 | let variants = enum_data 17 | .variants 18 | .into_iter() 19 | .map(|v| v.ident) 20 | .collect::>(); 21 | 22 | let res = quote! { 23 | impl From<#struct_name> for valkey_module::redisvalue::ValkeyValue { 24 | fn from(val: #struct_name) -> valkey_module::redisvalue::ValkeyValue { 25 | match val { 26 | #( 27 | #struct_name::#variants(v) => v.into(), 28 | )* 29 | } 30 | } 31 | } 32 | }; 33 | res.into() 34 | } 35 | 36 | /// Represent a single field attributes 37 | #[derive(Debug, Deserialize, Default)] 38 | struct FieldAttr { 39 | flatten: bool, 40 | } 41 | 42 | impl Parse for FieldAttr { 43 | fn parse(input: ParseStream) -> parse::Result { 44 | from_stream(config::JSONY, input) 45 | } 46 | } 47 | 48 | /// Generate [From] implementation for [ValkeyValue] for a struct. 49 | /// The generated code will create a [ValkeyValue::Map] element such that 50 | /// the keys are the fields names and the value are the result of 51 | /// running [Into] on each field value to convert it to [ValkeyValue]. 52 | fn struct_valkey_value(struct_name: Ident, struct_data: DataStruct) -> TokenStream { 53 | let fields = match struct_data.fields { 54 | Fields::Named(f) => f, 55 | _ => { 56 | return quote! {compile_error!("ValkeyValue derive can only be apply on struct with named fields.")}.into() 57 | } 58 | }; 59 | 60 | let fields = fields 61 | .named 62 | .into_iter() 63 | .map(|v| { 64 | let name = v 65 | .ident 66 | .ok_or("Field without a name is not supported.".to_owned())?; 67 | if v.attrs.len() > 1 { 68 | return Err("Expected at most a single attribute for each field".to_owned()); 69 | } 70 | let field_attr = v.attrs.into_iter().next().map_or( 71 | Ok::<_, String>(FieldAttr::default()), 72 | |attr| { 73 | let tokens = attr.tokens; 74 | let field_attr: FieldAttr = 75 | parse_macro_input::parse(tokens.into()).map_err(|e| format!("{e}"))?; 76 | Ok(field_attr) 77 | }, 78 | )?; 79 | Ok((name, field_attr)) 80 | }) 81 | .collect::, String>>(); 82 | 83 | let fields = match fields { 84 | Ok(f) => f, 85 | Err(e) => return quote! {compile_error!(#e)}.into(), 86 | }; 87 | 88 | let (fields, flattem_fields) = fields.into_iter().fold( 89 | (Vec::new(), Vec::new()), 90 | |(mut fields, mut flatten_fields), (field, attr)| { 91 | if attr.flatten { 92 | flatten_fields.push(field); 93 | } else { 94 | fields.push(field); 95 | } 96 | 97 | (fields, flatten_fields) 98 | }, 99 | ); 100 | 101 | let fields_names: Vec<_> = fields.iter().map(|v| v.to_string()).collect(); 102 | 103 | let res = quote! { 104 | impl From<#struct_name> for valkey_module::redisvalue::ValkeyValue { 105 | fn from(val: #struct_name) -> valkey_module::redisvalue::ValkeyValue { 106 | let mut fields: std::collections::BTreeMap = std::collections::BTreeMap::from([ 107 | #(( 108 | valkey_module::redisvalue::ValkeyValueKey::String(#fields_names.to_owned()), 109 | val.#fields.into() 110 | ), )* 111 | ]); 112 | #( 113 | let flatten_field: std::collections::BTreeMap = val.#flattem_fields.into(); 114 | fields.extend(flatten_field.into_iter()); 115 | )* 116 | valkey_module::redisvalue::ValkeyValue::OrderedMap(fields) 117 | } 118 | } 119 | 120 | impl From<#struct_name> for std::collections::BTreeMap { 121 | fn from(val: #struct_name) -> std::collections::BTreeMap { 122 | std::collections::BTreeMap::from([ 123 | #(( 124 | valkey_module::redisvalue::ValkeyValueKey::String(#fields_names.to_owned()), 125 | val.#fields.into() 126 | ), )* 127 | ]) 128 | } 129 | } 130 | }; 131 | res.into() 132 | } 133 | 134 | /// Implementation for [ValkeyValue] derive proc macro. 135 | /// Runs the relevant code generation base on the element 136 | /// the proc macro was used on. Currently supports Enums and 137 | /// structs. 138 | pub fn valkey_value(item: TokenStream) -> TokenStream { 139 | let struct_input: DeriveInput = parse_macro_input!(item); 140 | let struct_name = struct_input.ident; 141 | match struct_input.data { 142 | Data::Struct(s) => struct_valkey_value(struct_name, s), 143 | Data::Enum(e) => enum_valkey_value(struct_name, e), 144 | _ => quote! {compile_error!("ValkeyValue derive can only be apply on struct.")}.into(), 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/context/client.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | Context, RedisModuleClientInfo, RedisModule_DeauthenticateAndCloseClient, 3 | RedisModule_GetClientCertificate, RedisModule_GetClientId, RedisModule_GetClientInfoById, 4 | RedisModule_GetClientNameById, RedisModule_GetClientUserNameById, 5 | RedisModule_SetClientNameById, Status, ValkeyError, ValkeyResult, ValkeyString, ValkeyValue, 6 | }; 7 | use std::ffi::CStr; 8 | use std::os::raw::c_void; 9 | 10 | impl RedisModuleClientInfo { 11 | fn new() -> Self { 12 | Self { 13 | version: 1, 14 | flags: 0, 15 | id: 0, 16 | addr: [0; 46], 17 | port: 0, 18 | db: 0, 19 | } 20 | } 21 | } 22 | 23 | /// GetClientNameById, GetClientUserNameById and GetClientCertificate use autoMemoryAdd on the ValkeyModuleString pointer 24 | /// after the callback (command, server event handler, ...) these ValkeyModuleString pointers will be freed automatically 25 | impl Context { 26 | pub fn get_client_id(&self) -> u64 { 27 | unsafe { RedisModule_GetClientId.unwrap()(self.ctx) } 28 | } 29 | 30 | /// wrapper for RedisModule_GetClientNameById 31 | pub fn get_client_name_by_id(&self, client_id: u64) -> ValkeyResult { 32 | let client_name = unsafe { RedisModule_GetClientNameById.unwrap()(self.ctx, client_id) }; 33 | if client_name.is_null() { 34 | Err(ValkeyError::Str("Client/Client name is null")) 35 | } else { 36 | Ok(ValkeyString::from_redis_module_string( 37 | self.ctx, 38 | client_name, 39 | )) 40 | } 41 | } 42 | 43 | /// wrapper for RedisModule_GetClientNameById using current client ID 44 | pub fn get_client_name(&self) -> ValkeyResult { 45 | self.get_client_name_by_id(self.get_client_id()) 46 | } 47 | 48 | /// wrapper for RedisModule_SetClientNameById 49 | pub fn set_client_name_by_id(&self, client_id: u64, client_name: &ValkeyString) -> Status { 50 | let resp = unsafe { RedisModule_SetClientNameById.unwrap()(client_id, client_name.inner) }; 51 | Status::from(resp) 52 | } 53 | 54 | /// wrapper for RedisModule_SetClientNameById using current client ID 55 | pub fn set_client_name(&self, client_name: &ValkeyString) -> Status { 56 | self.set_client_name_by_id(self.get_client_id(), client_name) 57 | } 58 | 59 | /// wrapper for RedisModule_GetClientUserNameById 60 | pub fn get_client_username_by_id(&self, client_id: u64) -> ValkeyResult { 61 | let client_username = 62 | unsafe { RedisModule_GetClientUserNameById.unwrap()(self.ctx, client_id) }; 63 | if client_username.is_null() { 64 | Err(ValkeyError::Str("Client/Username is null")) 65 | } else { 66 | Ok(ValkeyString::from_redis_module_string( 67 | self.ctx, 68 | client_username, 69 | )) 70 | } 71 | } 72 | 73 | /// wrapper for RedisModule_GetClientUserNameById using current client ID 74 | pub fn get_client_username(&self) -> ValkeyResult { 75 | self.get_client_username_by_id(self.get_client_id()) 76 | } 77 | 78 | /// wrapper for RedisModule_GetClientCertificate 79 | pub fn get_client_cert(&self) -> ValkeyResult { 80 | let client_id = self.get_client_id(); 81 | let client_cert = unsafe { RedisModule_GetClientCertificate.unwrap()(self.ctx, client_id) }; 82 | if client_cert.is_null() { 83 | Err(ValkeyError::Str("Client/Cert is null")) 84 | } else { 85 | Ok(ValkeyString::from_redis_module_string( 86 | self.ctx, 87 | client_cert, 88 | )) 89 | } 90 | } 91 | 92 | /// wrapper for RedisModule_GetClientInfoById 93 | pub fn get_client_info_by_id(&self, client_id: u64) -> ValkeyResult { 94 | let mut mci = RedisModuleClientInfo::new(); 95 | let mci_ptr: *mut c_void = &mut mci as *mut _ as *mut c_void; 96 | unsafe { 97 | RedisModule_GetClientInfoById.unwrap()(mci_ptr, client_id); 98 | }; 99 | if mci_ptr.is_null() { 100 | Err(ValkeyError::Str("Client/Info is null")) 101 | } else { 102 | Ok(mci) 103 | } 104 | } 105 | 106 | /// wrapper for RedisModule_GetClientInfoById using current client ID 107 | pub fn get_client_info(&self) -> ValkeyResult { 108 | self.get_client_info_by_id(self.get_client_id()) 109 | } 110 | 111 | /// wrapper to get the client IP address from RedisModuleClientInfo 112 | pub fn get_client_ip_by_id(&self, client_id: u64) -> ValkeyResult { 113 | let client_info = self.get_client_info_by_id(client_id)?; 114 | let c_str_addr = unsafe { CStr::from_ptr(client_info.addr.as_ptr()) }; 115 | let ip_addr_as_string = c_str_addr.to_string_lossy().into_owned(); 116 | Ok(ip_addr_as_string) 117 | } 118 | 119 | /// wrapper to get the client IP address from RedisModuleClientInfo using current client ID 120 | pub fn get_client_ip(&self) -> ValkeyResult { 121 | self.get_client_ip_by_id(self.get_client_id()) 122 | } 123 | 124 | pub fn deauthenticate_and_close_client_by_id(&self, client_id: u64) -> Status { 125 | let resp = 126 | unsafe { RedisModule_DeauthenticateAndCloseClient.unwrap()(self.ctx, client_id) }; 127 | Status::from(resp) 128 | } 129 | 130 | pub fn deauthenticate_and_close_client(&self) -> Status { 131 | self.deauthenticate_and_close_client_by_id(self.get_client_id()) 132 | } 133 | 134 | pub fn config_get(&self, config: String) -> ValkeyResult { 135 | match self.call("CONFIG", &["GET", &config])? { 136 | ValkeyValue::Array(array) if array.len() == 2 => match &array[1] { 137 | ValkeyValue::SimpleString(val) => Ok(ValkeyString::create(None, val.clone())), 138 | _ => Err(ValkeyError::Str("Config value is not a string")), 139 | }, 140 | _ => Err(ValkeyError::Str("Unexpected CONFIG GET response")), 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/context/thread_safe.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Borrow; 2 | use std::cell::UnsafeCell; 3 | use std::ops::{Deref, DerefMut}; 4 | use std::ptr; 5 | 6 | use crate::context::blocked::BlockedClient; 7 | use crate::{raw, Context, ValkeyResult}; 8 | 9 | pub struct ValkeyGILGuardScope<'ctx, 'mutex, T, G: ValkeyLockIndicator> { 10 | _context: &'ctx G, 11 | mutex: &'mutex ValkeyGILGuard, 12 | } 13 | 14 | impl<'ctx, 'mutex, T, G: ValkeyLockIndicator> Deref for ValkeyGILGuardScope<'ctx, 'mutex, T, G> { 15 | type Target = T; 16 | 17 | fn deref(&self) -> &Self::Target { 18 | unsafe { &*self.mutex.obj.get() } 19 | } 20 | } 21 | 22 | impl<'ctx, 'mutex, T, G: ValkeyLockIndicator> DerefMut for ValkeyGILGuardScope<'ctx, 'mutex, T, G> { 23 | fn deref_mut(&mut self) -> &mut Self::Target { 24 | unsafe { &mut *self.mutex.obj.get() } 25 | } 26 | } 27 | 28 | /// Whenever the user gets a reference to a struct that 29 | /// implements this trait, it can assume that the Valkey GIL 30 | /// is held. Any struct that implements this trait can be 31 | /// used to retrieve objects which are GIL protected (see 32 | /// [RedisGILGuard] for more information) 33 | /// 34 | /// Notice that this trait only gives indication that the 35 | /// GIL is locked, unlike [RedisGILGuard] which protect data 36 | /// access and make sure the protected data is only accesses 37 | /// when the GIL is locked. 38 | /// 39 | /// # Safety 40 | /// 41 | /// In general this trait should not be implemented by the 42 | /// user, the crate knows when the Valkey GIL is held and will 43 | /// make sure to implement this trait correctly on different 44 | /// struct (such as [Context], [ConfigurationContext], [ContextGuard]). 45 | /// User might also decide to implement this trait but he should 46 | /// carefully consider that because it is easy to make mistakes, 47 | /// this is why the trait is marked as unsafe. 48 | pub unsafe trait ValkeyLockIndicator {} 49 | 50 | /// This struct allows to guard some data and makes sure 51 | /// the data is only access when the Valkey GIL is locked. 52 | /// From example, assuming you module want to save some 53 | /// statistics inside some global variable, but without the 54 | /// need to protect this variable with some mutex (because 55 | /// we know this variable is protected by Valkey lock). 56 | /// For example, look at examples/threads.rs 57 | pub struct ValkeyGILGuard { 58 | obj: UnsafeCell, 59 | } 60 | 61 | impl ValkeyGILGuard { 62 | pub fn new(obj: T) -> ValkeyGILGuard { 63 | ValkeyGILGuard { 64 | obj: UnsafeCell::new(obj), 65 | } 66 | } 67 | 68 | pub fn lock<'mutex, 'ctx, G: ValkeyLockIndicator>( 69 | &'mutex self, 70 | context: &'ctx G, 71 | ) -> ValkeyGILGuardScope<'ctx, 'mutex, T, G> { 72 | ValkeyGILGuardScope { 73 | _context: context, 74 | mutex: self, 75 | } 76 | } 77 | } 78 | 79 | impl Default for ValkeyGILGuard { 80 | fn default() -> Self { 81 | Self::new(T::default()) 82 | } 83 | } 84 | 85 | unsafe impl Sync for ValkeyGILGuard {} 86 | unsafe impl Send for ValkeyGILGuard {} 87 | 88 | pub struct ContextGuard { 89 | ctx: Context, 90 | } 91 | 92 | unsafe impl ValkeyLockIndicator for ContextGuard {} 93 | 94 | impl Drop for ContextGuard { 95 | fn drop(&mut self) { 96 | unsafe { 97 | raw::RedisModule_ThreadSafeContextUnlock.unwrap()(self.ctx.ctx); 98 | raw::RedisModule_FreeThreadSafeContext.unwrap()(self.ctx.ctx); 99 | }; 100 | } 101 | } 102 | 103 | impl Deref for ContextGuard { 104 | type Target = Context; 105 | 106 | fn deref(&self) -> &Self::Target { 107 | &self.ctx 108 | } 109 | } 110 | 111 | impl Borrow for ContextGuard { 112 | fn borrow(&self) -> &Context { 113 | &self.ctx 114 | } 115 | } 116 | 117 | /// A ``ThreadSafeContext`` can either be bound to a blocked client, or detached from any client. 118 | pub struct DetachedFromClient; 119 | 120 | pub struct ThreadSafeContext { 121 | pub(crate) ctx: *mut raw::RedisModuleCtx, 122 | 123 | /// This field is only used implicitly by `Drop`, so avoid a compiler warning 124 | #[allow(dead_code)] 125 | blocked_client: B, 126 | } 127 | 128 | unsafe impl Send for ThreadSafeContext {} 129 | unsafe impl Sync for ThreadSafeContext {} 130 | 131 | impl ThreadSafeContext { 132 | #[must_use] 133 | pub fn new() -> Self { 134 | let ctx = unsafe { raw::RedisModule_GetThreadSafeContext.unwrap()(ptr::null_mut()) }; 135 | Self { 136 | ctx, 137 | blocked_client: DetachedFromClient, 138 | } 139 | } 140 | } 141 | 142 | impl Default for ThreadSafeContext { 143 | fn default() -> Self { 144 | Self::new() 145 | } 146 | } 147 | 148 | impl ThreadSafeContext> { 149 | #[must_use] 150 | pub fn with_blocked_client(blocked_client: BlockedClient) -> Self { 151 | let ctx = unsafe { raw::RedisModule_GetThreadSafeContext.unwrap()(blocked_client.inner) }; 152 | Self { 153 | ctx, 154 | blocked_client, 155 | } 156 | } 157 | 158 | /// The Valkey modules API does not require locking for `Reply` functions, 159 | /// so we pass through its functionality directly. 160 | #[allow(clippy::must_use_candidate)] 161 | pub fn reply(&self, r: ValkeyResult) -> raw::Status { 162 | let ctx = Context::new(self.ctx); 163 | ctx.reply(r) 164 | } 165 | } 166 | 167 | impl ThreadSafeContext { 168 | /// All other APIs require locking the context, so we wrap it in a way 169 | /// similar to `std::sync::Mutex`. 170 | pub fn lock(&self) -> ContextGuard { 171 | unsafe { raw::RedisModule_ThreadSafeContextLock.unwrap()(self.ctx) }; 172 | let ctx = unsafe { raw::RedisModule_GetThreadSafeContext.unwrap()(ptr::null_mut()) }; 173 | let ctx = Context::new(ctx); 174 | ContextGuard { ctx } 175 | } 176 | } 177 | 178 | impl Drop for ThreadSafeContext { 179 | fn drop(&mut self) { 180 | unsafe { raw::RedisModule_FreeThreadSafeContext.unwrap()(self.ctx) }; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /examples/call.rs: -------------------------------------------------------------------------------- 1 | use valkey_module::{ 2 | valkey_module, BlockedClient, CallOptionResp, CallOptionsBuilder, CallReply, CallResult, 3 | Context, FutureCallReply, PromiseCallReply, ThreadSafeContext, ValkeyError, ValkeyResult, 4 | ValkeyString, ValkeyValue, 5 | }; 6 | 7 | use std::thread; 8 | use valkey_module::alloc::ValkeyAlloc; 9 | 10 | fn call_test(ctx: &Context, _: Vec) -> ValkeyResult { 11 | let res: String = ctx.call("ECHO", &["TEST"])?.try_into()?; 12 | if "TEST" != &res { 13 | return Err(ValkeyError::Str("Failed calling 'ECHO TEST'")); 14 | } 15 | 16 | let res: String = ctx.call("ECHO", vec!["TEST"].as_slice())?.try_into()?; 17 | if "TEST" != &res { 18 | return Err(ValkeyError::Str( 19 | "Failed calling 'ECHO TEST' dynamic str vec", 20 | )); 21 | } 22 | 23 | let res: String = ctx.call("ECHO", &[b"TEST"])?.try_into()?; 24 | if "TEST" != &res { 25 | return Err(ValkeyError::Str( 26 | "Failed calling 'ECHO TEST' with static [u8]", 27 | )); 28 | } 29 | 30 | let res: String = ctx.call("ECHO", vec![b"TEST"].as_slice())?.try_into()?; 31 | if "TEST" != &res { 32 | return Err(ValkeyError::Str( 33 | "Failed calling 'ECHO TEST' dynamic &[u8] vec", 34 | )); 35 | } 36 | 37 | let res: String = ctx.call("ECHO", &[&"TEST".to_string()])?.try_into()?; 38 | if "TEST" != &res { 39 | return Err(ValkeyError::Str("Failed calling 'ECHO TEST' with String")); 40 | } 41 | 42 | let res: String = ctx 43 | .call("ECHO", vec![&"TEST".to_string()].as_slice())? 44 | .try_into()?; 45 | if "TEST" != &res { 46 | return Err(ValkeyError::Str( 47 | "Failed calling 'ECHO TEST' dynamic &[u8] vec", 48 | )); 49 | } 50 | 51 | let res: String = ctx 52 | .call("ECHO", &[&ctx.create_string("TEST")])? 53 | .try_into()?; 54 | if "TEST" != &res { 55 | return Err(ValkeyError::Str( 56 | "Failed calling 'ECHO TEST' with ValkeyString", 57 | )); 58 | } 59 | 60 | let res: String = ctx 61 | .call("ECHO", vec![&ctx.create_string("TEST")].as_slice())? 62 | .try_into()?; 63 | if "TEST" != &res { 64 | return Err(ValkeyError::Str( 65 | "Failed calling 'ECHO TEST' with dynamic array of ValkeyString", 66 | )); 67 | } 68 | 69 | let call_options = CallOptionsBuilder::new().script_mode().errors_as_replies(); 70 | let res: CallResult = ctx.call_ext::<&[&str; 0], _>("SHUTDOWN", &call_options.build(), &[]); 71 | if let Err(err) = res { 72 | let error_msg = err.to_utf8_string().unwrap(); 73 | if !error_msg.contains("not allow") { 74 | return Err(ValkeyError::String(format!( 75 | "Failed to verify error messages, expected error message to contain 'not allow', error message: '{error_msg}'", 76 | ))); 77 | } 78 | } else { 79 | return Err(ValkeyError::Str("Failed to set script mode on call_ext")); 80 | } 81 | 82 | // test resp3 on call_ext 83 | let call_options = CallOptionsBuilder::new() 84 | .script_mode() 85 | .resp(CallOptionResp::Resp3) 86 | .errors_as_replies() 87 | .build(); 88 | ctx.call_ext::<_, CallResult>("HSET", &call_options, &["x", "foo", "bar"]) 89 | .map_err(|e| -> ValkeyError { e.into() })?; 90 | let res: CallReply = ctx 91 | .call_ext::<_, CallResult>("HGETALL", &call_options, &["x"]) 92 | .map_err(|e| -> ValkeyError { e.into() })?; 93 | if let CallReply::Map(map) = res { 94 | let res = map.iter().fold(Vec::new(), |mut vec, (key, val)| { 95 | if let CallReply::String(key) = key.unwrap() { 96 | vec.push(key.to_string().unwrap()); 97 | } 98 | if let CallReply::String(val) = val.unwrap() { 99 | vec.push(val.to_string().unwrap()); 100 | } 101 | vec 102 | }); 103 | if res != vec!["foo".to_string(), "bar".to_string()] { 104 | return Err(ValkeyError::String( 105 | "Reply of hgetall does not match expected value".into(), 106 | )); 107 | } 108 | } else { 109 | return Err(ValkeyError::String( 110 | "Did not get a set type on hgetall".into(), 111 | )); 112 | } 113 | 114 | Ok("pass".into()) 115 | } 116 | 117 | fn call_blocking_internal(ctx: &Context) -> PromiseCallReply { 118 | let call_options = CallOptionsBuilder::new().build_blocking(); 119 | ctx.call_blocking("blpop", &call_options, &["list", "1"]) 120 | } 121 | 122 | fn call_blocking_handle_future(ctx: &Context, f: FutureCallReply, blocked_client: BlockedClient) { 123 | let future_handler = f.set_unblock_handler(move |_ctx, reply| { 124 | let thread_ctx = ThreadSafeContext::with_blocked_client(blocked_client); 125 | thread_ctx.reply(reply.map_or_else(|e| Err(e.into()), |v| Ok((&v).into()))); 126 | }); 127 | future_handler.dispose(ctx); 128 | } 129 | 130 | fn call_blocking(ctx: &Context, _: Vec) -> ValkeyResult { 131 | let res = call_blocking_internal(ctx); 132 | match res { 133 | PromiseCallReply::Resolved(r) => r.map_or_else(|e| Err(e.into()), |v| Ok((&v).into())), 134 | PromiseCallReply::Future(f) => { 135 | let blocked_client = ctx.block_client(); 136 | call_blocking_handle_future(ctx, f, blocked_client); 137 | Ok(ValkeyValue::NoReply) 138 | } 139 | } 140 | } 141 | 142 | fn call_blocking_from_detach_ctx(ctx: &Context, _: Vec) -> ValkeyResult { 143 | let blocked_client = ctx.block_client(); 144 | thread::spawn(move || { 145 | let ctx_guard = valkey_module::MODULE_CONTEXT.lock(); 146 | let res = call_blocking_internal(&ctx_guard); 147 | match res { 148 | PromiseCallReply::Resolved(r) => { 149 | let thread_ctx = ThreadSafeContext::with_blocked_client(blocked_client); 150 | thread_ctx.reply(r.map_or_else(|e| Err(e.into()), |v| Ok((&v).into()))); 151 | } 152 | PromiseCallReply::Future(f) => { 153 | call_blocking_handle_future(&ctx_guard, f, blocked_client); 154 | } 155 | } 156 | }); 157 | Ok(ValkeyValue::NoReply) 158 | } 159 | 160 | ////////////////////////////////////////////////////// 161 | 162 | valkey_module! { 163 | name: "call", 164 | version: 1, 165 | allocator: (ValkeyAlloc, ValkeyAlloc), 166 | data_types: [], 167 | commands: [ 168 | ["call.test", call_test, "", 0, 0, 0], 169 | ["call.blocking", call_blocking, "", 0, 0, 0], 170 | ["call.blocking_from_detached_ctx", call_blocking_from_detach_ctx, "", 0, 0, 0], 171 | ], 172 | } 173 | -------------------------------------------------------------------------------- /examples/configuration.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{ 2 | atomic::{AtomicBool, AtomicI64}, 3 | Mutex, 4 | }; 5 | 6 | use lazy_static::lazy_static; 7 | use valkey_module::alloc::ValkeyAlloc; 8 | use valkey_module::{ 9 | configuration::{ConfigurationContext, ConfigurationFlags}, 10 | enum_configuration, valkey_module, ConfigurationValue, Context, ValkeyError, ValkeyGILGuard, 11 | ValkeyResult, ValkeyString, ValkeyValue, 12 | }; 13 | 14 | enum_configuration! { 15 | #[derive(PartialEq)] 16 | enum EnumConfiguration { 17 | Val1 = 1, 18 | Val2 = 2, 19 | } 20 | } 21 | 22 | lazy_static! { 23 | static ref NUM_OF_CONFIGURATION_CHANGES: ValkeyGILGuard = ValkeyGILGuard::default(); 24 | static ref CONFIGURATION_I64: ValkeyGILGuard = ValkeyGILGuard::default(); 25 | static ref CONFIGURATION_REJECT_I64: ValkeyGILGuard = ValkeyGILGuard::default(); 26 | static ref CONFIGURATION_ATOMIC_I64: AtomicI64 = AtomicI64::new(1); 27 | static ref CONFIGURATION_VALKEY_STRING: ValkeyGILGuard = 28 | ValkeyGILGuard::new(ValkeyString::create(None, "default")); 29 | static ref CONFIGURATION_REJECT_VALKEY_STRING: ValkeyGILGuard = 30 | ValkeyGILGuard::new(ValkeyString::create(None, "default")); 31 | static ref CONFIGURATION_STRING: ValkeyGILGuard = ValkeyGILGuard::new("default".into()); 32 | static ref CONFIGURATION_MUTEX_STRING: Mutex = Mutex::new("default".into()); 33 | static ref CONFIGURATION_ATOMIC_BOOL: AtomicBool = AtomicBool::default(); 34 | static ref CONFIGURATION_BOOL: ValkeyGILGuard = ValkeyGILGuard::default(); 35 | static ref CONFIGURATION_REJECT_BOOL: ValkeyGILGuard = ValkeyGILGuard::default(); 36 | static ref CONFIGURATION_ENUM: ValkeyGILGuard = 37 | ValkeyGILGuard::new(EnumConfiguration::Val1); 38 | static ref CONFIGURATION_REJECT_ENUM: ValkeyGILGuard = 39 | ValkeyGILGuard::new(EnumConfiguration::Val1); 40 | static ref CONFIGURATION_MUTEX_ENUM: Mutex = 41 | Mutex::new(EnumConfiguration::Val1); 42 | } 43 | 44 | fn on_configuration_changed>( 45 | config_ctx: &ConfigurationContext, 46 | _name: &str, 47 | _val: &'static T, 48 | ) { 49 | let mut val = NUM_OF_CONFIGURATION_CHANGES.lock(config_ctx); 50 | *val += 1 51 | } 52 | 53 | // Custom on_set handlers to add validation and conditionally 54 | // reject upon config change. 55 | fn on_string_config_set>( 56 | config_ctx: &ConfigurationContext, 57 | _name: &str, 58 | val: &'static T, 59 | ) -> Result<(), ValkeyError> { 60 | let v = val.get(config_ctx); 61 | if v.to_string_lossy().contains("rejectvalue") { 62 | return Err(ValkeyError::Str("Rejected from custom string validation.")); 63 | } 64 | Ok(()) 65 | } 66 | fn on_i64_config_set>( 67 | config_ctx: &ConfigurationContext, 68 | _name: &str, 69 | val: &'static T, 70 | ) -> Result<(), ValkeyError> { 71 | let v = val.get(config_ctx); 72 | if v == 123 { 73 | return Err(ValkeyError::Str("Rejected from custom i64 validation.")); 74 | } 75 | Ok(()) 76 | } 77 | fn on_bool_config_set>( 78 | config_ctx: &ConfigurationContext, 79 | _name: &str, 80 | val: &'static T, 81 | ) -> Result<(), ValkeyError> { 82 | let v = val.get(config_ctx); 83 | if v == false { 84 | return Err(ValkeyError::Str("Rejected from custom bool validation.")); 85 | } 86 | Ok(()) 87 | } 88 | fn on_enum_config_set>( 89 | config_ctx: &ConfigurationContext, 90 | _name: &str, 91 | val: &'static T, 92 | ) -> Result<(), ValkeyError> { 93 | let v = val.get(config_ctx); 94 | if v == EnumConfiguration::Val2 { 95 | return Err(ValkeyError::Str("Rejected from custom enum validation.")); 96 | } 97 | Ok(()) 98 | } 99 | 100 | fn num_changes(ctx: &Context, _: Vec) -> ValkeyResult { 101 | let val = NUM_OF_CONFIGURATION_CHANGES.lock(ctx); 102 | Ok(ValkeyValue::Integer(*val)) 103 | } 104 | 105 | ////////////////////////////////////////////////////// 106 | 107 | valkey_module! { 108 | name: "configuration", 109 | version: 1, 110 | allocator: (ValkeyAlloc, ValkeyAlloc), 111 | data_types: [], 112 | commands: [ 113 | ["configuration.num_changes", num_changes, "", 0, 0, 0], 114 | ], 115 | configurations: [ 116 | i64: [ 117 | ["i64", &*CONFIGURATION_I64, 10, 0, 1000, ConfigurationFlags::DEFAULT, Some(Box::new(on_configuration_changed))], 118 | ["reject_i64", &*CONFIGURATION_REJECT_I64, 10, 0, 1000, ConfigurationFlags::DEFAULT, Some(Box::new(on_configuration_changed)), Some(Box::new(on_i64_config_set::>))], 119 | ["atomic_i64", &*CONFIGURATION_ATOMIC_I64, 10, 0, 1000, ConfigurationFlags::DEFAULT, Some(Box::new(on_configuration_changed))], 120 | ], 121 | string: [ 122 | ["valkey_string", &*CONFIGURATION_VALKEY_STRING, "default", ConfigurationFlags::DEFAULT, Some(Box::new(on_configuration_changed))], 123 | ["reject_valkey_string", &*CONFIGURATION_REJECT_VALKEY_STRING, "default", ConfigurationFlags::DEFAULT, Some(Box::new(on_configuration_changed)), Some(Box::new(on_string_config_set::>))], 124 | ["string", &*CONFIGURATION_STRING, "default", ConfigurationFlags::DEFAULT, Some(Box::new(on_configuration_changed::))], 125 | ["mutex_string", &*CONFIGURATION_MUTEX_STRING, "default", ConfigurationFlags::DEFAULT, Some(Box::new(on_configuration_changed::))], 126 | ], 127 | bool: [ 128 | ["atomic_bool", &*CONFIGURATION_ATOMIC_BOOL, true, ConfigurationFlags::DEFAULT, Some(Box::new(on_configuration_changed))], 129 | ["bool", &*CONFIGURATION_BOOL, true, ConfigurationFlags::DEFAULT, Some(Box::new(on_configuration_changed))], 130 | ["reject_bool", &*CONFIGURATION_REJECT_BOOL, true, ConfigurationFlags::DEFAULT, Some(Box::new(on_configuration_changed)), Some(Box::new(on_bool_config_set::>))], 131 | ], 132 | enum: [ 133 | ["enum", &*CONFIGURATION_ENUM, EnumConfiguration::Val1, ConfigurationFlags::DEFAULT, Some(Box::new(on_configuration_changed))], 134 | ["reject_enum", &*CONFIGURATION_REJECT_ENUM, EnumConfiguration::Val1, ConfigurationFlags::DEFAULT, Some(Box::new(on_configuration_changed)), Some(Box::new(on_enum_config_set::>))], 135 | ["enum_mutex", &*CONFIGURATION_MUTEX_ENUM, EnumConfiguration::Val1, ConfigurationFlags::DEFAULT, Some(Box::new(on_configuration_changed))], 136 | ], 137 | module_args_as_configuration: true, 138 | ] 139 | } 140 | -------------------------------------------------------------------------------- /src/logging.rs: -------------------------------------------------------------------------------- 1 | use crate::raw; 2 | use std::ffi::CString; 3 | use std::ptr; 4 | use strum_macros::AsRefStr; 5 | 6 | const NOT_INITIALISED_MESSAGE: &str = "Valkey module hasn't been initialised."; 7 | 8 | /// [ValkeyLogLevel] is a level of logging which can be used when 9 | /// logging with Redis. See [raw::RedisModule_Log] and the official 10 | /// valkey [reference](https://valkey.io/topics/modules-api-ref/). 11 | #[derive(Clone, Copy, Debug, AsRefStr)] 12 | #[strum(serialize_all = "snake_case")] 13 | pub enum ValkeyLogLevel { 14 | Debug, 15 | Notice, 16 | Verbose, 17 | Warning, 18 | } 19 | 20 | impl From for ValkeyLogLevel { 21 | fn from(value: log::Level) -> Self { 22 | match value { 23 | log::Level::Error | log::Level::Warn => Self::Warning, 24 | log::Level::Info => Self::Notice, 25 | log::Level::Debug => Self::Verbose, 26 | log::Level::Trace => Self::Debug, 27 | } 28 | } 29 | } 30 | 31 | pub(crate) fn log_internal>( 32 | ctx: *mut raw::RedisModuleCtx, 33 | level: L, 34 | message: &str, 35 | ) { 36 | if cfg!(test) { 37 | return; 38 | } 39 | 40 | let level = CString::new(level.into().as_ref()).unwrap(); 41 | let fmt = CString::new(message).unwrap(); 42 | unsafe { 43 | raw::RedisModule_Log.expect(NOT_INITIALISED_MESSAGE)(ctx, level.as_ptr(), fmt.as_ptr()) 44 | } 45 | } 46 | 47 | /// This function should be used when a callback is returning a critical error 48 | /// to the caller since cannot load or save the data for some critical reason. 49 | #[allow(clippy::not_unsafe_ptr_arg_deref)] 50 | pub fn log_io_error(io: *mut raw::RedisModuleIO, level: ValkeyLogLevel, message: &str) { 51 | if cfg!(test) { 52 | return; 53 | } 54 | let level = CString::new(level.as_ref()).unwrap(); 55 | let fmt = CString::new(message).unwrap(); 56 | unsafe { 57 | raw::RedisModule_LogIOError.expect(NOT_INITIALISED_MESSAGE)( 58 | io, 59 | level.as_ptr(), 60 | fmt.as_ptr(), 61 | ) 62 | } 63 | } 64 | 65 | /// Log a message to the Valkey log with the given log level, without 66 | /// requiring a context. This prevents Valkey from including the module 67 | /// name in the logged message. 68 | pub fn log>(level: ValkeyLogLevel, message: T) { 69 | log_internal(ptr::null_mut(), level, message.as_ref()); 70 | } 71 | 72 | /// Log a message to Valkey at the [ValkeyLogLevel::Debug] level. 73 | pub fn log_debug>(message: T) { 74 | log(ValkeyLogLevel::Debug, message.as_ref()); 75 | } 76 | 77 | /// Log a message to Valkey at the [ValkeyLogLevel::Notice] level. 78 | pub fn log_notice>(message: T) { 79 | log(ValkeyLogLevel::Notice, message.as_ref()); 80 | } 81 | 82 | /// Log a message to Valkey at the [ValkeyLogLevel::Verbose] level. 83 | pub fn log_verbose>(message: T) { 84 | log(ValkeyLogLevel::Verbose, message.as_ref()); 85 | } 86 | 87 | /// Log a message to Valkey at the [ValkeyLogLevel::Warning] level. 88 | pub fn log_warning>(message: T) { 89 | log(ValkeyLogLevel::Warning, message.as_ref()); 90 | } 91 | 92 | /// The [log] crate implementation of logging. 93 | pub mod standard_log_implementation { 94 | use std::sync::{atomic::Ordering, OnceLock}; 95 | 96 | use crate::ValkeyError; 97 | 98 | use super::*; 99 | use log::{Metadata, Record, SetLoggerError}; 100 | 101 | /// The struct which has an implementation of the [log] crate's 102 | /// logging interface. 103 | /// 104 | /// # Note 105 | /// 106 | /// Valkey does not support logging at the [log::Level::Error] level, 107 | /// so logging at this level will be converted to logging at the 108 | /// [log::Level::Warn] level under the hood. 109 | struct ValkeyGlobalLogger(*mut raw::RedisModuleCtx); 110 | 111 | // The pointer of the Global logger can only be changed once during 112 | // the startup. Once one of the [std::sync::OnceLock] or 113 | // [std::sync::OnceCell] is stabilised, we can remove these unsafe 114 | // trait implementations in favour of using the aforementioned safe 115 | // types. 116 | unsafe impl Send for ValkeyGlobalLogger {} 117 | unsafe impl Sync for ValkeyGlobalLogger {} 118 | 119 | /// Sets this logger as a global logger. Use this method to set 120 | /// up the logger. If this method is never called, the default 121 | /// logger is used which redirects the logging to the standard 122 | /// input/output streams. 123 | /// 124 | /// # Note 125 | /// 126 | /// The logging context (the module context [raw::RedisModuleCtx]) 127 | /// is set by the [crate::redis_module] macro. If another context 128 | /// should be used, please consider using the [setup_for_context] 129 | /// method instead. 130 | /// 131 | /// In case this function is invoked before the initialisation, and 132 | /// so without the valkey module context, no context will be used for 133 | /// the logging, however, the logger will be set. 134 | /// 135 | /// # Example 136 | /// 137 | /// This function may be called on a module startup, within the 138 | /// module initialisation function (specified in the 139 | /// [crate::redis_module] as the `init` argument, which will be used 140 | /// for the module initialisation and will be passed to the 141 | /// [raw::Export_RedisModule_Init] function when loading the 142 | /// module). 143 | #[allow(dead_code)] 144 | pub fn setup() -> Result<(), ValkeyError> { 145 | let pointer = crate::MODULE_CONTEXT.ctx.load(Ordering::Relaxed); 146 | if pointer.is_null() { 147 | return Err(ValkeyError::Str(NOT_INITIALISED_MESSAGE)); 148 | } 149 | setup_for_context(pointer) 150 | .map_err(|e| ValkeyError::String(format!("Couldn't set up the logger: {e}"))) 151 | } 152 | 153 | fn logger(context: *mut raw::RedisModuleCtx) -> &'static ValkeyGlobalLogger { 154 | static LOGGER: OnceLock = OnceLock::new(); 155 | LOGGER.get_or_init(|| ValkeyGlobalLogger(context)) 156 | } 157 | 158 | /// The same as [setup] but sets the custom module context. 159 | #[allow(dead_code)] 160 | pub fn setup_for_context(context: *mut raw::RedisModuleCtx) -> Result<(), SetLoggerError> { 161 | log::set_logger(logger(context)).map(|()| log::set_max_level(log::LevelFilter::Trace)) 162 | } 163 | 164 | impl log::Log for ValkeyGlobalLogger { 165 | fn enabled(&self, _: &Metadata) -> bool { 166 | true 167 | } 168 | 169 | fn log(&self, record: &Record) { 170 | if !self.enabled(record.metadata()) { 171 | return; 172 | } 173 | 174 | let message = match record.level() { 175 | log::Level::Debug | log::Level::Trace => { 176 | format!( 177 | "'{}' {}:{}: {}", 178 | record.module_path().unwrap_or_default(), 179 | record.file().unwrap_or("Unknown"), 180 | record.line().unwrap_or(0), 181 | record.args() 182 | ) 183 | } 184 | _ => record.args().to_string(), 185 | }; 186 | 187 | log_internal(self.0, record.level(), &message); 188 | } 189 | 190 | fn flush(&self) { 191 | // The flushing isn't required for the Valkey logging. 192 | } 193 | } 194 | } 195 | pub use standard_log_implementation::*; 196 | -------------------------------------------------------------------------------- /src/context/blocked.rs: -------------------------------------------------------------------------------- 1 | use crate::redismodule::AUTH_HANDLED; 2 | use crate::{raw, Context, ValkeyError, ValkeyString}; 3 | use std::os::raw::{c_int, c_void}; 4 | 5 | // Callback types for handling blocked client operations 6 | // Currently supports authentication reply callback for block_client_on_auth 7 | #[derive(Debug)] 8 | pub enum ReplyCallback { 9 | Auth(fn(&Context, ValkeyString, ValkeyString, Option<&T>) -> Result), 10 | } 11 | 12 | #[derive(Debug)] 13 | struct BlockedClientPrivateData { 14 | reply_callback: Option>, 15 | free_callback: Option>, 16 | data: Option>, 17 | } 18 | 19 | // Callback type for freeing private data associated with a blocked client 20 | type FreePrivateDataCallback = fn(&Context, T); 21 | 22 | pub struct BlockedClient { 23 | pub(crate) inner: *mut raw::RedisModuleBlockedClient, 24 | reply_callback: Option>, 25 | free_callback: Option>, 26 | data: Option>, 27 | } 28 | 29 | #[allow(dead_code)] 30 | unsafe extern "C" fn auth_reply_wrapper( 31 | ctx: *mut raw::RedisModuleCtx, 32 | username: *mut raw::RedisModuleString, 33 | password: *mut raw::RedisModuleString, 34 | err: *mut *mut raw::RedisModuleString, 35 | ) -> c_int { 36 | let context = Context::new(ctx); 37 | let ctx_ptr = std::ptr::NonNull::new_unchecked(ctx); 38 | let username = ValkeyString::new(Some(ctx_ptr), username); 39 | let password = ValkeyString::new(Some(ctx_ptr), password); 40 | 41 | let module_private_data = context.get_blocked_client_private_data(); 42 | if module_private_data.is_null() { 43 | panic!("[auth_reply_wrapper] Module private data is null; this should not happen!"); 44 | } 45 | 46 | let user_private_data = &*(module_private_data as *const BlockedClientPrivateData); 47 | 48 | let cb = match user_private_data.reply_callback.as_ref() { 49 | Some(ReplyCallback::Auth(cb)) => cb, 50 | None => panic!("[auth_reply_wrapper] Reply callback is null; this should not happen!"), 51 | }; 52 | 53 | let data_ref = user_private_data.data.as_deref(); 54 | 55 | match cb(&context, username, password, data_ref) { 56 | Ok(result) => result, 57 | Err(error) => { 58 | let error_msg = ValkeyString::create_and_retain(&error.to_string()); 59 | *err = error_msg.inner; 60 | AUTH_HANDLED 61 | } 62 | } 63 | } 64 | 65 | #[allow(dead_code)] 66 | unsafe extern "C" fn free_callback_wrapper( 67 | ctx: *mut raw::RedisModuleCtx, 68 | module_private_data: *mut c_void, 69 | ) { 70 | let context = Context::new(ctx); 71 | 72 | if module_private_data.is_null() { 73 | panic!("[free_callback_wrapper] Module private data is null; this should not happen!"); 74 | } 75 | 76 | let user_private_data = Box::from_raw(module_private_data as *mut BlockedClientPrivateData); 77 | 78 | // Execute free_callback only if both callback and data exist 79 | // Note: free_callback can exist without data - this is a valid state 80 | if let Some(free_cb) = user_private_data.free_callback { 81 | if let Some(data) = user_private_data.data { 82 | free_cb(&context, *data); 83 | } 84 | } 85 | } 86 | 87 | // We need to be able to send the inner pointer to another thread 88 | unsafe impl Send for BlockedClient {} 89 | 90 | impl BlockedClient { 91 | pub(crate) fn new(inner: *mut raw::RedisModuleBlockedClient) -> Self { 92 | Self { 93 | inner, 94 | reply_callback: None, 95 | free_callback: None, 96 | data: None, 97 | } 98 | } 99 | 100 | #[allow(dead_code)] 101 | pub(crate) fn with_auth_callback( 102 | inner: *mut raw::RedisModuleBlockedClient, 103 | auth_reply_callback: fn( 104 | &Context, 105 | ValkeyString, 106 | ValkeyString, 107 | Option<&T>, 108 | ) -> Result, 109 | free_callback: Option>, 110 | ) -> Self 111 | where 112 | T: 'static, 113 | { 114 | Self { 115 | inner, 116 | reply_callback: Some(ReplyCallback::Auth(auth_reply_callback)), 117 | free_callback, 118 | data: None, 119 | } 120 | } 121 | 122 | /// Sets private data for the blocked client. 123 | /// 124 | /// # Arguments 125 | /// * `data` - The private data to store 126 | /// 127 | /// # Returns 128 | /// * `Ok(())` - If the private data was successfully set 129 | /// * `Err(ValkeyError)` - If setting the private data failed (e.g., no free callback) 130 | pub fn set_blocked_private_data(&mut self, data: T) -> Result<(), ValkeyError> { 131 | if self.free_callback.is_none() { 132 | return Err(ValkeyError::Str( 133 | "Cannot set private data without a free callback - this would leak memory", 134 | )); 135 | } 136 | self.data = Some(Box::new(data)); 137 | Ok(()) 138 | } 139 | 140 | /// Aborts the blocked client operation 141 | /// 142 | /// # Returns 143 | /// * `Ok(())` - If the blocked client was successfully aborted 144 | /// * `Err(ValkeyError)` - If the abort operation failed 145 | pub fn abort(mut self) -> Result<(), ValkeyError> { 146 | unsafe { 147 | // Clear references to data and callbacks 148 | self.data = None; 149 | self.reply_callback = None; 150 | self.free_callback = None; 151 | 152 | if raw::RedisModule_AbortBlock.unwrap()(self.inner) == raw::REDISMODULE_OK as c_int { 153 | // Prevent the normal Drop from running 154 | self.inner = std::ptr::null_mut(); 155 | Ok(()) 156 | } else { 157 | Err(ValkeyError::Str("Failed to abort blocked client")) 158 | } 159 | } 160 | } 161 | } 162 | 163 | impl Drop for BlockedClient { 164 | fn drop(&mut self) { 165 | if !self.inner.is_null() { 166 | let callback_data_ptr = if self.reply_callback.is_some() || self.free_callback.is_some() 167 | { 168 | Box::into_raw(Box::new(BlockedClientPrivateData { 169 | reply_callback: self.reply_callback.take(), 170 | free_callback: self.free_callback.take(), 171 | data: self.data.take(), 172 | })) as *mut c_void 173 | } else { 174 | std::ptr::null_mut() 175 | }; 176 | 177 | unsafe { 178 | raw::RedisModule_UnblockClient.unwrap()(self.inner, callback_data_ptr); 179 | } 180 | } 181 | } 182 | } 183 | 184 | impl Context { 185 | #[must_use] 186 | pub fn block_client(&self) -> BlockedClient { 187 | let blocked_client = unsafe { 188 | raw::RedisModule_BlockClient.unwrap()( 189 | self.ctx, // ctx 190 | None, // reply_func 191 | None, // timeout_func 192 | None, 0, 193 | ) 194 | }; 195 | 196 | BlockedClient::new(blocked_client) 197 | } 198 | 199 | /// Blocks a client during authentication and registers callbacks 200 | /// 201 | /// Wrapper around ValkeyModule_BlockClientOnAuth. Used for asynchronous authentication 202 | /// processing. 203 | /// 204 | /// # Arguments 205 | /// * `auth_reply_callback` - Callback executed when authentication completes 206 | /// * `free_callback` - Optional callback for cleaning up private data 207 | /// 208 | /// # Returns 209 | /// * `BlockedClient` - Handle to manage the blocked client 210 | #[must_use] 211 | #[cfg(all(any( 212 | feature = "min-redis-compatibility-version-7-2", 213 | feature = "min-valkey-compatibility-version-8-0" 214 | ),))] 215 | pub fn block_client_on_auth( 216 | &self, 217 | auth_reply_callback: fn( 218 | &Context, 219 | ValkeyString, 220 | ValkeyString, 221 | Option<&T>, 222 | ) -> Result, 223 | free_callback: Option>, 224 | ) -> BlockedClient { 225 | unsafe { 226 | let blocked_client = raw::RedisModule_BlockClientOnAuth.unwrap()( 227 | self.ctx, 228 | Some(auth_reply_wrapper::), 229 | Some(free_callback_wrapper::), 230 | ); 231 | 232 | BlockedClient::with_auth_callback(blocked_client, auth_reply_callback, free_callback) 233 | } 234 | } 235 | 236 | /// Retrieves the private data associated with a blocked client in the current context. 237 | /// This is an internal function used primarily by reply callbacks to access user-provided data. 238 | /// 239 | /// # Safety 240 | /// This function returns a raw pointer that must be properly cast to the expected type. 241 | /// The caller must ensure the pointer is not null before dereferencing. 242 | /// 243 | /// # Implementation Detail 244 | /// Wraps the Valkey Module C API function `ValkeyModule_GetBlockedClientPrivateData` 245 | pub(crate) fn get_blocked_client_private_data(&self) -> *mut c_void { 246 | unsafe { raw::RedisModule_GetBlockedClientPrivateData.unwrap()(self.ctx) } 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | commands: 4 | early-returns: 5 | steps: 6 | - run: 7 | name: Early return if this is a docs build 8 | command: | 9 | if [[ $CIRCLE_BRANCH == *docs ]]; then 10 | echo "Identifies as documents PR, no testing required." 11 | circleci step halt 12 | fi 13 | - run: 14 | name: Early return if this branch should ignore CI 15 | command: | 16 | if [[ $CIRCLE_BRANCH == *noci ]]; then 17 | echo "Identifies as actively ignoring CI, no testing required." 18 | circleci step halt 19 | fi 20 | 21 | early-return-for-forked-pull-requests: 22 | description: >- 23 | If this build is from a fork, stop executing the current job and return success. 24 | This is useful to avoid steps that will fail due to missing credentials. 25 | steps: 26 | - run: 27 | name: Early return if this build is from a forked PR 28 | command: | 29 | if [[ -n "$CIRCLE_PR_NUMBER" ]]; then 30 | echo "Nothing to do for forked PRs, so marking this step successful" 31 | circleci step halt 32 | fi 33 | 34 | setup-executor: 35 | steps: 36 | - run: 37 | name: Setup executor 38 | command: | 39 | apt-get -qq update 40 | apt-get -q install -y git openssh-client curl ca-certificates make tar gzip 41 | bash <(curl -fsSL https://raw.githubusercontent.com/docker/docker-install/master/install.sh) 42 | - setup_remote_docker: 43 | version: 20.10.14 44 | docker_layer_caching: true 45 | 46 | checkout-all: 47 | steps: 48 | - checkout 49 | - run: 50 | name: Checkout submodules 51 | command: git submodule update --init --recursive 52 | 53 | setup-automation: 54 | steps: 55 | - run: 56 | name: Setup automation 57 | command: | 58 | git submodule update --init deps/readies 59 | if [[ $(uname -s) == Darwin ]]; then rm -f /usr/local/bin/python3; fi 60 | ./deps/readies/bin/getpy3 61 | - run: 62 | name: Setup automation (part 2) 63 | shell: /bin/bash -l -eo pipefail 64 | command: | 65 | ls -l /usr/local/bin/python* || true 66 | echo "python3: $(command -v python3)" 67 | python3 --version 68 | python3 -m pip list 69 | 70 | install-prerequisites: 71 | parameters: 72 | redis_version: 73 | type: string 74 | default: "7.2" 75 | getredis_params: 76 | type: string 77 | default: "" 78 | steps: 79 | - setup-automation 80 | - run: 81 | name: Setup build environment 82 | shell: /bin/bash -l -eo pipefail 83 | command: | 84 | ./sbin/system-setup.py 85 | bash -l -c "make info" 86 | - run: 87 | name: Install Redis 88 | shell: /bin/bash -l -eo pipefail 89 | command: | 90 | export HOMEBREW_NO_AUTO_UPDATE=1 91 | ./deps/readies/bin/getredis -v '<>' --force <> 92 | redis-server --version 93 | 94 | build-steps: 95 | parameters: 96 | build_params: 97 | type: string 98 | default: "" 99 | test_params: 100 | type: string 101 | default: "" 102 | redis_version: 103 | type: string 104 | default: "7.2" 105 | getredis_params: 106 | type: string 107 | default: "" 108 | steps: 109 | - early-returns 110 | - checkout-all 111 | - install-prerequisites: 112 | redis_version: <> 113 | getredis_params: <> 114 | - run: 115 | name: patch macos tests # Avoid AVX with Regex with CircleCI since virtualization layer doesn't support it. Use sed to replace the relevant entry in cargo.toml 116 | command: | 117 | if [[ $(uname -s) == Darwin ]]; then sed -i 's/regex = "1"/regex = { version = "1", features = ["perf", "unicode"], default-features = false }/g' Cargo.toml; fi 118 | cat Cargo.toml 119 | - restore_cache: 120 | keys: 121 | - v3-dependencies-{{ arch }}-{{ checksum "Cargo.toml" }} 122 | - run: 123 | name: Check formatting 124 | shell: /bin/bash -l -eo pipefail 125 | command: make lint 126 | - run: 127 | name: Build debug 128 | shell: /bin/bash -l -eo pipefail 129 | command: make build DEBUG=1 <> 130 | - run: 131 | name: Build release 132 | shell: /bin/bash -l -eo pipefail 133 | command: make build <> 134 | - save_cache: 135 | key: v3-dependencies-{{ arch }}-{{ checksum "Cargo.toml" }} 136 | paths: 137 | - "~/.cargo" 138 | - "./target" 139 | - run: 140 | name: Run tests 141 | shell: /bin/bash -l -eo pipefail 142 | command: make test 143 | 144 | build-platforms-steps: 145 | parameters: 146 | platform: 147 | type: string 148 | steps: 149 | - early-returns 150 | - setup-executor 151 | - checkout-all 152 | - setup-automation 153 | - run: 154 | name: Build for platform 155 | shell: /bin/bash -l -eo pipefail 156 | command: | 157 | cd build/docker 158 | make build OSNICK="<>" VERSION="$CIRCLE_TAG" BRANCH="$CIRCLE_BRANCH" TEST=1 SHOW=1 159 | 160 | vm-build-platforms-steps: 161 | parameters: 162 | platform: 163 | type: string 164 | steps: 165 | - early-returns 166 | - checkout 167 | - setup-automation 168 | - run: 169 | name: Install Docker 170 | shell: /bin/bash -l -eo pipefail 171 | command: ./deps/readies/bin/getdocker 172 | - run: 173 | name: Build for platform 174 | command: | 175 | cd build/docker 176 | make build OSNICK="<>" VERSION="$CIRCLE_TAG" BRANCH="$CIRCLE_BRANCH" TEST=1 SHOW=1 177 | no_output_timeout: 30m 178 | 179 | #---------------------------------------------------------------------------------------------------------------------------------- 180 | 181 | jobs: 182 | build-linux-debian: 183 | docker: 184 | - image: redisfab/rmbuilder:6.2.7-x64-bullseye 185 | steps: 186 | - build-steps 187 | 188 | build-platforms: 189 | parameters: 190 | platform: 191 | type: string 192 | docker: 193 | - image: debian:bullseye 194 | steps: 195 | - build-platforms-steps: 196 | platform: <> 197 | 198 | build-arm-platforms: 199 | parameters: 200 | platform: 201 | type: string 202 | machine: 203 | image: ubuntu-2004:202101-01 204 | resource_class: arm.medium 205 | steps: 206 | - vm-build-platforms-steps: 207 | platform: <> 208 | 209 | build-macos-x64: 210 | macos: 211 | xcode: 14.2.0 212 | resource_class: macos.x86.medium.gen2 213 | steps: 214 | - run: brew install openssl 215 | - build-steps 216 | 217 | build-macos-m1: 218 | macos: 219 | xcode: 14.2.0 220 | resource_class: macos.m1.large.gen1 221 | steps: 222 | - build-steps 223 | 224 | #---------------------------------------------------------------------------------------------------------------------------------- 225 | 226 | on-any-branch: &on-any-branch 227 | filters: 228 | branches: 229 | only: /.*/ 230 | tags: 231 | only: /.*/ 232 | 233 | always: &always 234 | filters: 235 | branches: 236 | only: /.*/ 237 | tags: 238 | only: /.*/ 239 | 240 | never: &never 241 | filters: 242 | branches: 243 | ignore: /.*/ 244 | tags: 245 | ignore: /.*/ 246 | 247 | on-master: &on-master 248 | filters: 249 | branches: 250 | only: master 251 | tags: 252 | ignore: /.*/ 253 | 254 | on-integ-branch: &on-integ-branch 255 | filters: 256 | branches: 257 | only: 258 | - master 259 | - /^\d+\.\d+.*$/ 260 | - /^feature-.*$/ 261 | tags: 262 | ignore: /.*/ 263 | 264 | on-integ-branch-cron: &on-integ-branch-cron 265 | filters: 266 | branches: 267 | only: 268 | - master 269 | - /^\d+\.\d+.*$/ 270 | - /^feature-.*$/ 271 | 272 | not-on-integ-branch: ¬-on-integ-branch 273 | filters: 274 | branches: 275 | ignore: 276 | - master 277 | - /^\d+\.\d+.*$/ 278 | - /^feature-.*$/ 279 | tags: 280 | ignore: /.*/ 281 | 282 | on-version-tags: &on-version-tags 283 | filters: 284 | branches: 285 | ignore: /.*/ 286 | tags: 287 | only: /^v[0-9].*/ 288 | 289 | on-integ-and-version-tags: &on-integ-and-version-tags 290 | filters: 291 | branches: 292 | only: 293 | - master 294 | - /^\d+\.\d+.*$/ 295 | - /^feature-.*$/ 296 | tags: 297 | only: /^v[0-9].*/ 298 | 299 | #---------------------------------------------------------------------------------------------------------------------------------- 300 | 301 | workflows: 302 | version: 2 303 | default-flow: 304 | jobs: 305 | - build-linux-debian: 306 | name: build 307 | <<: *not-on-integ-branch 308 | - build-platforms: 309 | <<: *on-integ-and-version-tags 310 | context: common 311 | matrix: 312 | parameters: 313 | platform: [jammy, focal, bionic, xenial, rocky8, centos7, bullseye, amzn2] 314 | - build-arm-platforms: 315 | <<: *on-integ-and-version-tags 316 | context: common 317 | matrix: 318 | parameters: 319 | platform: [jammy, bionic, focal] 320 | - build-macos-x64: 321 | <<: *always 322 | - build-macos-m1: 323 | <<: *on-integ-and-version-tags 324 | --------------------------------------------------------------------------------