/* An example module for attaching metadata to keys. * * This example lets tests create metadata-key classes and then SET and GET metadata * to keys. The 8-byte slot stores a handle to a module-managed allocation; here * we use to attach a string per-key. * * The module pre-registers several metadata classes during initialization and exposes * the following commands (via RedisModule_CreateCommand): * * 1) KEYMETA.REGISTER <4-char-id> [FLAGS] * Register a new metadata-key class during module load. * Returns the index (Returned from RedisModule_CreateKeyMetaClass) * On failure, returns nil * In a real module it should be registered "automatically" via OnLoad. * * FLAGS (colon-separated): * KEEPONCOPY - Keep metadata on COPY operation * KEEPONRENAME - Keep metadata on RENAME operation * KEEPONMOVE - Keep metadata on MOVE operation * UNLINKFREE - Use unlink callback for async free * RDBLOAD - Enable rdb_load callback (metadata can be loaded from RDB) * RDBSAVE - Enable rdb_save callback (metadata can be saved to RDB) * ALLOWIGNORE - Enable ALLOW_IGNORE flag (graceful discard on load if * class not registered or no rdb_load callback) * * Example: > keymeta.register KMT1 1 KEEPONCOPY:KEEPONRENAME:ALLOWIGNORE:RDBLOAD:RDBSAVE * Example: > keymeta.register KMT2 1 ALLOWIGNORE * * 2) KEYMETA.SET <4-char-id> * Set the string value as metadata to given key. * Note: * - If already set earlier, then it is expected that it will released before setting a * new string. That is why this command should start with trying to get first * metadata for given key. * * 3) KEYMETA.GET <4-char-id> * Get the metadata attached to the key for the given class. * Returns a string attached to the given key. Or nil if nothing is attached. * * 4) KEYMETA.UNREGISTER <4-char-id> * This will mark the key metadata class as released. It can later be reused again * by the same class (consider comment above). * Return REDISMODULE_OK/REDISMODULE_ERR. * * 5) KEYMETA.ACTIVE * Return total number of active metadata at the moment. */ #include "redismodule.h" #include #include #include /* Virtualize class IDs for testing. Values: 0 unused, 1..7 used, -1 released */ RedisModuleKeyMetaClassId class_ids[8] = { 0 }; /* Mapping from 4-char-id to class-id */ typedef struct { char name[5]; /* 4 chars + null terminator */ RedisModuleKeyMetaClassId class_id; } ClassMapping; #define MAX_CLASS_MAPPINGS 8 static ClassMapping class_mappings[MAX_CLASS_MAPPINGS]; static int num_class_mappings = 0; /* Reverse lookup: given a class_id, find the 4-char-id name */ static const char* lookupClassName(RedisModuleKeyMetaClassId class_id) { for (int i = 0; i < num_class_mappings; i++) { if (class_mappings[i].class_id == class_id) { return class_mappings[i].name; } } return NULL; } /* Track active metadata instances (not yet freed) */ static long long active_metadata_count = 0; /* Helper functions for class mapping */ /* Add a mapping from 4-char-id to class-id */ static int addClassMapping(const char *name, RedisModuleKeyMetaClassId class_id) { if (num_class_mappings >= MAX_CLASS_MAPPINGS) { return 0; /* No space */ } strncpy(class_mappings[num_class_mappings].name, name, 4); class_mappings[num_class_mappings].name[4] = '\0'; class_mappings[num_class_mappings].class_id = class_id; num_class_mappings++; return 1; } /* Lookup class-id by 4-char-id. Returns -1 if not found. */ static RedisModuleKeyMetaClassId lookupClassId(const char *name) { for (int i = 0; i < num_class_mappings; i++) { if (strncmp(class_mappings[i].name, name, 4) == 0) { return class_mappings[i].class_id; } } return -1; } /* Remove a mapping by 4-char-id */ static int removeClassMapping(const char *name) { for (int i = 0; i < num_class_mappings; i++) { if (strncmp(class_mappings[i].name, name, 4) == 0) { /* Shift remaining entries down */ for (int j = i; j < num_class_mappings - 1; j++) { class_mappings[j] = class_mappings[j + 1]; } num_class_mappings--; return 1; } } return 0; } /* Callback functions for metadata lifecycle */ /* Copy callback - called when a key is copied */ static int KeyMetaCopyCallback(RedisModuleKeyOptCtx *ctx, uint64_t *meta) { REDISMODULE_NOT_USED(ctx); char *str = (char *)*meta; /* Note, condition is redundant since cb only invoked when meta != reset_value */ if (str) { char *new_str = strdup(str); *meta = (uint64_t)new_str; active_metadata_count++; /* New metadata instance created */ } return 1; /* Keep metadata */ } /* Rename callback - called when a key is renamed. */ static int KeyMetaRenameDiscardCallback(RedisModuleKeyOptCtx *ctx, uint64_t *meta) { REDISMODULE_NOT_USED(ctx); REDISMODULE_NOT_USED(meta); return 0; } /* Unlink callback - called when a key is unlinked */ static void KeyMetaUnlinkCallback(RedisModuleKeyOptCtx *ctx, uint64_t *meta) { /* Let's challenge and free early on before free callback */ /* Note, condition is redundant since cb only invoked when meta != reset_value */ if (*meta != 0) { char *str = (char *)*meta; free(str); *meta = 0; /* Set to reset_value !!! */ active_metadata_count--; /* Metadata instance freed */ } REDISMODULE_NOT_USED(ctx); } /* Free callback - called when metadata needs to be freed */ static void KeyMetaFreeCallback(const char *keyname, uint64_t meta) { REDISMODULE_NOT_USED(keyname); /* Note, condition is redundant since cb only invoked when meta != reset_value */ if (meta != 0) { char *str = (char *)meta; free(str); active_metadata_count--; /* Metadata instance freed */ } } static int KeyMetaMoveDiscardCallback(RedisModuleKeyOptCtx *ctx, uint64_t *meta) { REDISMODULE_NOT_USED(ctx); REDISMODULE_NOT_USED(meta); return 0; /* discard metadata */ } /* RDB Save Callback - Serialize metadata to RDB * This callback is invoked during RDB save to write the metadata value. * * Parameters: * - rdb: RedisModuleIO context for writing to RDB * - value: The kvobj (key-value object) - not used in this implementation * - meta: Pointer to the 8-byte metadata value (pointer to our string) */ static void KeyMetaRDBSaveCallback(RedisModuleIO *rdb, void *value, uint64_t *meta) { REDISMODULE_NOT_USED(value); /* If metadata is NULL (reset_value), don't save anything */ if (*meta == 0) return; /* Extract the string from the metadata pointer */ char *metadata_string = (char *)*meta; /* Save the string to RDB using SaveStringBuffer */ RedisModule_SaveStringBuffer(rdb, metadata_string, strlen(metadata_string)); /* Save more silly data */ RedisModule_SaveSigned(rdb, 1); RedisModule_SaveFloat(rdb, 1.5); RedisModule_SaveLongDouble(rdb, 0.333333333333333333L); } /* RDB Load Callback - Deserialize metadata from RDB * This callback is invoked during RDB load to read the metadata value. * * Parameters: * - rdb: RedisModuleIO context for reading from RDB * - meta: Pointer to store the loaded 8-byte metadata value * - encver: Encoding version (class version from RDB) * * Returns: * - 1: Attach metadata to key (success) * - 0: Ignore/skip metadata (not an error) * - -1: Error - abort RDB load */ static int KeyMetaRDBLoadCallback(RedisModuleIO *rdb, uint64_t *meta, int encver) { REDISMODULE_NOT_USED(encver); /* Load the string from RDB using LoadStringBuffer */ size_t len; char *loaded_string = RedisModule_LoadStringBuffer(rdb, &len); if (loaded_string == NULL) { /* Error loading string */ return -1; } /* Allocate and copy the string (LoadStringBuffer returns a buffer that must be freed) */ char *metadata_string = malloc(len + 1); if (metadata_string == NULL) { RedisModule_Free(loaded_string); return -1; } memcpy(metadata_string, loaded_string, len); metadata_string[len] = '\0'; RedisModule_Free(loaded_string); /* Load the additional data that was saved (must match rdb_save) */ int64_t signed_val = RedisModule_LoadSigned(rdb); float float_val = RedisModule_LoadFloat(rdb); long double ldouble_val = RedisModule_LoadLongDouble(rdb); /* We don't use these values, just need to consume them from the stream */ (void)signed_val; (void)float_val; (void)ldouble_val; /* Store the pointer in metadata */ *meta = (uint64_t)metadata_string; active_metadata_count++; /* New metadata instance created */ /* Return 1 to attach metadata to the key */ return 1; } /* AOF Rewrite Callback - Common implementation for all classes * This callback is invoked during AOF rewrite to emit commands that will * recreate the metadata when the AOF is loaded. * * Parameters: * - aof: RedisModuleIO context for writing to AOF * - value: The kvobj (key-value object) - not used in this implementation * - meta: The 8-byte metadata value (pointer to our string) * - class_id: The class ID for this metadata */ static void KeyMetaAOFRewriteCallback_Class(RedisModuleIO *aof, void *value, uint64_t meta, RedisModuleKeyMetaClassId class_id) { REDISMODULE_NOT_USED(value); /* If metadata is NULL (reset_value), don't emit anything */ if (meta == 0) return; /* Extract the string from the metadata pointer */ char *metadata_string = (char *)meta; /* Lookup the 9-byte-id name for this class */ const char *class_name = lookupClassName(class_id); if (!class_name) { /* This shouldn't happen, but handle gracefully */ return; } /* Get the key name from the AOF IO context */ const RedisModuleString *key = RedisModule_GetKeyNameFromIO(aof); if (!key) { /* Key name not available - shouldn't happen during AOF rewrite */ return; } /* Emit the KEYMETA.SET command to recreate this metadata * Format: KEYMETA.SET <9-byte-id> */ RedisModule_EmitAOF(aof, "KEYMETA.SET", "csc", class_name, /* c: 9-byte-id (C string) */ key, /* s: key name (RedisModuleString) */ metadata_string); /* c: metadata value (C string) */ } /* Individual AOF rewrite callbacks for each class (1-7) * Each callback wraps the common implementation with its specific class ID */ static void KeyMetaAOFRewriteCb1(RedisModuleIO *aof, void *value, uint64_t meta) { KeyMetaAOFRewriteCallback_Class(aof, value, meta, 1); } static void KeyMetaAOFRewriteCb2(RedisModuleIO *aof, void *value, uint64_t meta) { KeyMetaAOFRewriteCallback_Class(aof, value, meta, 2); } static void KeyMetaAOFRewriteCb3(RedisModuleIO *aof, void *value, uint64_t meta) { KeyMetaAOFRewriteCallback_Class(aof, value, meta, 3); } static void KeyMetaAOFRewriteCb4(RedisModuleIO *aof, void *value, uint64_t meta) { KeyMetaAOFRewriteCallback_Class(aof, value, meta, 4); } static void KeyMetaAOFRewriteCb5(RedisModuleIO *aof, void *value, uint64_t meta) { KeyMetaAOFRewriteCallback_Class(aof, value, meta, 5); } static void KeyMetaAOFRewriteCb6(RedisModuleIO *aof, void *value, uint64_t meta) { KeyMetaAOFRewriteCallback_Class(aof, value, meta, 6); } static void KeyMetaAOFRewriteCb7(RedisModuleIO *aof, void *value, uint64_t meta) { KeyMetaAOFRewriteCallback_Class(aof, value, meta, 7); } /* KEYMETA.REGISTER <4-char-id> [KEEPONCOPY:KEEPONRENAME:UNLINKFREE:ALLOWIGNORE:NORDBLOAD:NORDBSAVE] */ static int KeyMetaRegister_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { if (argc < 3 || argc > 4) { return RedisModule_WrongArity(ctx); } /* argv[1]: key metadata class name */ size_t namelen; const char *metaname = RedisModule_StringPtrLen(argv[1], &namelen); /* argv[2]: key metadata class version */ long long metaver; if (RedisModule_StringToLongLong(argv[2], &metaver) != REDISMODULE_OK) { RedisModule_ReplyWithError(ctx, "ERR invalid version number"); return REDISMODULE_OK; } /* Parse optional callback flags */ int keep_on_copy = 0, keep_on_rename = 0, unlink_free = 0, keep_on_move = 0; int allow_ignore = 0; /* Default: ALLOW_IGNORE disabled */ int rdb_load = 0; /* Default: rdb_load disabled */ int rdb_save = 0; /* Default: rdb_save disabled */ if (argc == 4) { const char *flags = RedisModule_StringPtrLen(argv[3], NULL); if (strstr(flags, "KEEPONCOPY")) keep_on_copy = 1; if (strstr(flags, "KEEPONRENAME")) keep_on_rename = 1; if (strstr(flags, "UNLINKFREE")) unlink_free = 1; if (strstr(flags, "KEEPONMOVE")) keep_on_move = 1; if (strstr(flags, "ALLOWIGNORE")) allow_ignore = 1; /* Enable ALLOW_IGNORE */ if (strstr(flags, "RDBLOAD")) rdb_load = 1; /* Enable rdb_load */ if (strstr(flags, "RDBSAVE")) rdb_save = 1; /* Enable rdb_save */ } /* Setup configuration */ RedisModuleKeyMetaClassConfig config = {0}; config.version = REDISMODULE_KEY_META_VERSION; config.flags = allow_ignore ? (1 << REDISMODULE_META_ALLOW_IGNORE) : 0; config.reset_value = (uint64_t)NULL; /* NULL pointer means no resource to free */ config.rdb_load = rdb_load ? KeyMetaRDBLoadCallback : NULL; config.rdb_save = rdb_save ? KeyMetaRDBSaveCallback : NULL; switch (num_class_mappings + 1) { /* distinct cb per class */ case 1: config.aof_rewrite = KeyMetaAOFRewriteCb1; break; case 2: config.aof_rewrite = KeyMetaAOFRewriteCb2; break; case 3: config.aof_rewrite = KeyMetaAOFRewriteCb3; break; case 4: config.aof_rewrite = KeyMetaAOFRewriteCb4; break; case 5: config.aof_rewrite = KeyMetaAOFRewriteCb5; break; case 6: config.aof_rewrite = KeyMetaAOFRewriteCb6; break; case 7: config.aof_rewrite = KeyMetaAOFRewriteCb7; break; default: config.aof_rewrite = NULL; break; } config.free = KeyMetaFreeCallback; config.copy = keep_on_copy ? KeyMetaCopyCallback : NULL; config.rename = keep_on_rename ? NULL : KeyMetaRenameDiscardCallback; config.move = keep_on_move ? NULL : KeyMetaMoveDiscardCallback; config.defrag = NULL; config.unlink = unlink_free ? KeyMetaUnlinkCallback : NULL; config.mem_usage = NULL; config.free_effort = NULL; /* Create the metadata class */ RedisModuleKeyMetaClassId class_id = RedisModule_CreateKeyMetaClass(ctx, metaname, (int)metaver, &config); if (class_id < 0) { RedisModule_ReplyWithError(ctx, "ERR failed to create metadata class"); return REDISMODULE_OK; } else { /* Store the mapping from 9-byte-id to class-id */ if (!addClassMapping(metaname, class_id)) { RedisModule_ReplyWithError(ctx, "ERR failed to store class mapping"); return REDISMODULE_OK; } RedisModule_ReplyWithLongLong(ctx, class_id); } return REDISMODULE_OK; } /* KEYMETA.SET <9-byte-id> */ static int KeyMetaSet_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { if (argc != 4) { return RedisModule_WrongArity(ctx); } /* Parse arguments */ const char *metaname = RedisModule_StringPtrLen(argv[1], NULL); RedisModuleString *keyname = argv[2]; const char *value = RedisModule_StringPtrLen(argv[3], NULL); /* Lookup the metadata class by name */ RedisModuleKeyMetaClassId class_id = lookupClassId(metaname); if (class_id < 0) { RedisModule_ReplyWithError(ctx, "ERR metadata class not found"); return REDISMODULE_OK; } /* Open the key for writing */ RedisModuleKey *key = RedisModule_OpenKey(ctx, keyname, REDISMODULE_READ | REDISMODULE_WRITE); if (RedisModule_KeyType(key) == REDISMODULE_KEYTYPE_EMPTY) { RedisModule_ReplyWithNull(ctx); RedisModule_CloseKey(key); return REDISMODULE_OK; } /* Check if metadata already exists and free it first. * * Note: The caller is responsible for retrieving and freeing any existing * pointer-based metadata before RM_SetKeyMeta() to a new value */ uint64_t meta = 0; if (RedisModule_GetKeyMeta(class_id, key, &meta) == REDISMODULE_OK) { if (meta != 0) { free((char *)meta); active_metadata_count--; /* Old metadata freed */ } } char *new_str = strdup(value); int res = RedisModule_SetKeyMeta(class_id, key, (uint64_t)new_str); if (res == REDISMODULE_OK) { active_metadata_count++; /* New metadata instance created */ } RedisModule_CloseKey(key); if (res == REDISMODULE_OK) { RedisModule_ReplyWithSimpleString(ctx, "OK"); } else { free(new_str); RedisModule_ReplyWithError(ctx, "ERR failed to set metadata"); } return REDISMODULE_OK; } /* KEYMETA.GET <9-byte-id> */ static int KeyMetaGet_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { if (argc != 3) { return RedisModule_WrongArity(ctx); } /* Parse arguments */ const char *metaname = RedisModule_StringPtrLen(argv[1], NULL); RedisModuleString *keyname = argv[2]; /* Lookup the metadata class by name */ RedisModuleKeyMetaClassId class_id = lookupClassId(metaname); if (class_id < 0) { RedisModule_ReplyWithError(ctx, "ERR metadata class not found"); return REDISMODULE_OK; } /* Open the key for reading */ RedisModuleKey *key = RedisModule_OpenKey(ctx, keyname, REDISMODULE_READ); if (RedisModule_KeyType(key) == REDISMODULE_KEYTYPE_EMPTY) { RedisModule_ReplyWithNull(ctx); RedisModule_CloseKey(key); return REDISMODULE_OK; } /* Get the metadata */ uint64_t meta = 0; int result = RedisModule_GetKeyMeta(class_id, key, &meta); RedisModule_CloseKey(key); if (result == REDISMODULE_OK && meta != 0) { char *str = (char *)meta; RedisModule_ReplyWithCString(ctx, str); } else { RedisModule_ReplyWithNull(ctx); } return REDISMODULE_OK; } /* KEYMETA.UNREGISTER <9-byte-id> */ static int KeyMetaUnregister_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { if (argc != 2) { return RedisModule_WrongArity(ctx); } /* Parse arguments */ const char *metaname = RedisModule_StringPtrLen(argv[1], NULL); /* Lookup the metadata class by name */ RedisModuleKeyMetaClassId class_id = lookupClassId(metaname); if (class_id < 0) { RedisModule_ReplyWithError(ctx, "ERR metadata class not found"); return REDISMODULE_OK; } /* Release the metadata class */ int result = RedisModule_ReleaseKeyMetaClass(class_id); if (result == REDISMODULE_OK) { /* Remove the mapping */ removeClassMapping(metaname); RedisModule_ReplyWithSimpleString(ctx, "OK"); } else { RedisModule_ReplyWithError(ctx, "ERR failed to unregister class"); } return REDISMODULE_OK; } /* KEYMETA.ACTIVE * Returns the total number of active metadata instances that haven't been freed yet. * This is useful for testing to verify that metadata is properly cleaned up. */ static int KeyMetaActive_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { if (argc != 1) { return RedisModule_WrongArity(ctx); } REDISMODULE_NOT_USED(argv); RedisModule_ReplyWithLongLong(ctx, active_metadata_count); return REDISMODULE_OK; } /* Module initialization */ int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { REDISMODULE_NOT_USED(argv); REDISMODULE_NOT_USED(argc); if (RedisModule_Init(ctx, "test_metakey", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR) { return REDISMODULE_ERR; } /* Register commands */ if (RedisModule_CreateCommand(ctx, "keymeta.register", KeyMetaRegister_RedisCommand, "write", 0, 0, 0) == REDISMODULE_ERR) { return REDISMODULE_ERR; } if (RedisModule_CreateCommand(ctx, "keymeta.set", KeyMetaSet_RedisCommand, "write deny-oom", 1, 1, 1) == REDISMODULE_ERR) { return REDISMODULE_ERR; } if (RedisModule_CreateCommand(ctx, "keymeta.get", KeyMetaGet_RedisCommand, "readonly", 1, 1, 1) == REDISMODULE_ERR) { return REDISMODULE_ERR; } if (RedisModule_CreateCommand(ctx, "keymeta.unregister", KeyMetaUnregister_RedisCommand, "write", 0, 0, 0) == REDISMODULE_ERR) { return REDISMODULE_ERR; } if (RedisModule_CreateCommand(ctx, "keymeta.active", KeyMetaActive_RedisCommand, "readonly fast", 0, 0, 0) == REDISMODULE_ERR) { return REDISMODULE_ERR; } return REDISMODULE_OK; } int RedisModule_OnUnload(RedisModuleCtx *ctx) { REDISMODULE_NOT_USED(ctx); long unsigned int i; for (i = 0 ; i < sizeof(class_ids) / sizeof(class_ids[0]); i++) { if (class_ids[i] > 0) RedisModule_ReleaseKeyMetaClass(class_ids[i]); } return REDISMODULE_OK; }