# ============================================================================ # Key Metadata (keymeta) Test Suite # ============================================================================ # # Tests the Redis module key metadata framework: up to 7 independent metadata # classes (IDs 1-7) can be attached to keys. Class ID 0 is reserved for key # expiration. # # The following features are sensitive to Key Metadata and are tested here: # # - KEY EXPIRATION (class ID 0) # - Stored at ((uint64_t *)kv) - 1 (first metadata slot) # - Managed via db->expires dictionary # - Must be preserved/updated when kvobj is reallocated # - HASH FIELD EXPIRATION (HFE) # - NOT in kvobj metadata slots (Maybe in the future...) # - Managed via db->hexpires ebuckets (holds direct kvobj pointer) # - Must be removed before kvobj reallocation (hashTypeRemoveFromExpires) # and restored after (hashTypeAddToExpires) # - MODULE METADATA (class IDs 1-7) # - Defines metadata lifecycle via callbacks # - EMBEDDED STRINGS vs. REGULAR OBJECTS # - Short strings and numbers are embedded into kvobj # - The rest are kept as distinct objects # - LAZYFREE # ============================================================================ set testmodule [file normalize tests/modules/test_keymeta.so] # Helper procedure to convert class ID to 4-char-id name proc cname {cid} { return "KMT$cid" } # Helper procedure to check if a class should keep metadata for a given operation proc shouldKeep {cid operation classesSpec} { upvar $classesSpec specs set spec $specs($cid) switch $operation { "copy" { return [string match "*KEEPONCOPY*" $spec] } "rename" { return [string match "*KEEPONRENAME*" $spec] } "move" { return [string match "*KEEPONMOVE*" $spec] } default { return 0 } } } # Helper procedure to setup a key with metadata proc setupKeyMeta {keyname numClasses expiryBefore expiryAfter} { # Set expiry if requested if {$expiryBefore} { r expire $keyname 10000 assert_range [r ttl $keyname] 9990 10000 } # Set metadata for all classes for {set i 1} {$i <= $numClasses} {incr i} { # Set twice to verify overwrite behavior r keymeta.set [cname $i] $keyname "blabla$i" assert_equal [r keymeta.get [cname $i] $keyname] "blabla$i" r keymeta.set [cname $i] $keyname "meta$i" } # Verify metadata was set correctly for {set i 1} {$i <= $numClasses} {incr i} { assert_equal [r keymeta.get [cname $i] $keyname] "meta$i" } if {$expiryAfter} { r expire $keyname 10000 assert_range [r ttl $keyname] 9990 10000 } if {$expiryBefore} { assert_range [r ttl $keyname] 9990 10000 } } # Helper procedure to verify metadata after an operation proc verifyKeyMeta {keyname operation numClasses hasExpiry classesSpec} { upvar $classesSpec specs # Verify expiry if {$hasExpiry} { assert_range [r ttl $keyname] 9990 10000 } # Verify metadata based on class spec for {set i 1} {$i <= $numClasses} {incr i} { set expected [expr {[shouldKeep $i $operation specs] ? "meta$i" : ""}] assert_equal [r keymeta.get [cname $i] $keyname] $expected } } proc flushallAndVerifyCleanup {} { r flushall # Verify all metadata is cleaned up properly assert_equal [r keymeta.active] 0 } start_server {tags {"modules" "external:skip" "cluster:skip"} overrides {enable-debug-command yes}} { r module load $testmodule array set classesSpec {} set classesSpec(1) "KEEPONCOPY:KEEPONRENAME:KEEPONMOVE:ALLOWIGNORE:RDBLOAD:RDBSAVE" set classesSpec(2) "KEEPONCOPY:KEEPONRENAME:UNLINKFREE:ALLOWIGNORE:RDBLOAD:RDBSAVE" set classesSpec(3) "KEEPONCOPY:ALLOWIGNORE:RDBLOAD:RDBSAVE" set classesSpec(4) "ALLOWIGNORE:RDBLOAD:RDBSAVE" set classesSpec(5) "KEEPONRENAME:KEEPONMOVE:ALLOWIGNORE:RDBLOAD:RDBSAVE" set classesSpec(6) "KEEPONRENAME:ALLOWIGNORE:RDBLOAD:RDBSAVE" set classesSpec(7) "KEEPONMOVE:UNLINKFREE:ALLOWIGNORE:RDBLOAD:RDBSAVE" array set classes {} for {set cid 1} {$cid <= 7} {incr cid} { set spec $classesSpec($cid) set classes($cid) [r keymeta.register [cname $cid] 1 $spec] puts "Registered class $cid with spec $spec" assert_equal $classes($cid) $cid } # Validates metadata behavior across COPY/RENAME/MOVE operations # with varying numbers of metadata classes (1-7), key expiration states, # key types (string/hash), hash field expiration, and metadata class flags # (KEEPONCOPY, KEEPONRENAME, KEEPONMOVE). for {set numClasses 1} {$numClasses < 8} {incr numClasses} { foreach expiryBefore {0 1} { foreach expiryAfter {0 1} { set hasExpiry [expr {$expiryBefore || $expiryAfter}] set expiryStr "expiryBefore=$expiryBefore, expiryAfter=$expiryAfter)" # Test COPY operation test "KEYMETA - copy key-string with $numClasses classes, $expiryStr" { foreach value { 3 "value1" [string repeat "ABCD" 1000]} { r select 0 r del k1 k2 r set k1 $value setupKeyMeta k1 $numClasses $expiryBefore $expiryAfter # Copy: r copy k1 k2 # Verify: assert_equal [r get k1] $value assert_equal [r get k2] $value # Verify expiry and metadata verifyKeyMeta k2 "copy" $numClasses $hasExpiry classesSpec flushallAndVerifyCleanup } } test "KEYMETA - copy key-hash with $numClasses classes, $expiryStr" { r select 0 r del h1 h2 r HSET h1 field1 "value1" field2 "value2" r hexpire h1 10000 FIELDS 1 field1 setupKeyMeta h1 $numClasses $expiryBefore $expiryAfter # Copy: r copy h1 h2 # Verify: verifyKeyMeta h2 "copy" $numClasses $hasExpiry classesSpec assert_range [r httl h1 FIELDS 1 field1] 9999 10000 assert_range [r httl h2 FIELDS 1 field1] 9999 10000 flushallAndVerifyCleanup } # Test RENAME operation test "KEYMETA - rename key-string with $numClasses classes, $expiryStr" { foreach value { 3 "value1" [string repeat "ABCD" 1000]} { r select 0 r del k1 k2 r set k1 $value setupKeyMeta k1 $numClasses $expiryBefore $expiryAfter # Rename: r rename k1 k2 # Verify: assert_equal [r exists k1] 0 assert_equal [r get k2] $value # Verify expiry and metadata verifyKeyMeta k2 "rename" $numClasses $hasExpiry classesSpec flushallAndVerifyCleanup } } test "KEYMETA - rename key-hash with $numClasses classes, $expiryStr" { r select 0 r del h1 h2 r HSET h1 field1 "value1" field2 "value2" r hexpire h1 10000 FIELDS 1 field1 setupKeyMeta h1 $numClasses $expiryBefore $expiryAfter # Rename: r rename h1 h2 # Verify: assert_equal [r exists h1] 0 assert_range [r httl h2 FIELDS 1 field1] 9999 10000 verifyKeyMeta h2 "rename" $numClasses $hasExpiry classesSpec flushallAndVerifyCleanup } # Test MOVE operation test "KEYMETA - move key-string with $numClasses classes, $expiryStr" { foreach value { 3 "value1" [string repeat "ABCD" 1000]} { r select 9 r del k1 r select 0 r del k1 r set k1 $value setupKeyMeta k1 $numClasses $expiryBefore $expiryAfter # Perform move assert_equal [r move k1 9] 1 # Verify key moved assert_equal [r exists k1] 0 r select 9 assert_equal [r get k1] $value # Verify expiry and metadata verifyKeyMeta k1 "move" $numClasses $hasExpiry classesSpec r select 0 flushallAndVerifyCleanup } } test "KEYMETA - move key-hash with $numClasses classes, $expiryStr" { r select 9 r del h1 r select 0 r del h1 r HSET h1 field1 "value1" field2 "value2" r hexpire h1 10000 FIELDS 1 field1 setupKeyMeta h1 $numClasses $expiryBefore $expiryAfter assert_range [r httl h1 FIELDS 1 field1] 9999 10000 assert_equal [r move h1 9] 1 assert_equal [r exists h1] 0 r select 9 assert_range [r httl h1 FIELDS 1 field1] 9999 10000 verifyKeyMeta h1 "move" $numClasses $hasExpiry classesSpec r select 0 flushallAndVerifyCleanup } } } } test "KEYMETA - Verify active metadata count on copy" { for {set cid 1} {$cid < 7} {incr cid} { set numAlloc 0 flushallAndVerifyCleanup set dupOnCopy [shouldKeep $cid "copy" classesSpec] r set k1 "v1" r keymeta.set [cname $cid] k1 "meta1" assert_equal [r keymeta.active] [incr numAlloc] r keymeta.set [cname $cid] k1 "meta1b" assert_equal [r keymeta.active] $numAlloc r copy k1 k1copy assert_equal [r keymeta.active] [incr numAlloc $dupOnCopy] r del k1 assert_equal [r keymeta.active] [incr numAlloc -1] r del k1copy assert_equal [r keymeta.active] 0 } } test "KEYMETA - Verify active metadata count on rename" { for {set cid 1} {$cid <= 7} {incr cid} { set numAlloc 0 flushallAndVerifyCleanup set keepOnRename [shouldKeep $cid "rename" classesSpec] set discOnRename [expr {!$keepOnRename}] r set k1 "v1" r keymeta.set [cname $cid] k1 "meta1" assert_equal [r keymeta.active] [incr numAlloc] r rename k1 k1_renamed assert_equal [r keymeta.active] [incr numAlloc -$discOnRename] r del k1_renamed assert_equal [r keymeta.active] 0 } } test "KEYMETA - Verify active metadata count on move" { for {set cid 1} {$cid <= 7} {incr cid} { set numAlloc 0 r select 0 flushallAndVerifyCleanup set keepOnMove [shouldKeep $cid "move" classesSpec] set discOnMove [expr {!$keepOnMove}] # Create keys with metadata in DB 0 r set k1 "v1" r keymeta.set [cname $cid] k1 "meta1" assert_equal [r keymeta.active] [incr numAlloc] # Move: metadata discarded if !keepOnMove r move k1 9 set active [r keymeta.active] assert_equal [r keymeta.active] [incr numAlloc -$discOnMove] # Cleanup r select 9 r del k1 r select 0 assert_equal [r keymeta.active] 0 } } test "KEYMETA - Verify metadta cleanup on lazyfree" { r config set lazyfree-lazy-user-del yes # Class 2 has UNLINKFREE flag, so it should call unlink callback when lazyfree is enabled # Class 1 does not have UNLINKFREE flag, so it should only call free callback foreach {cid} { 1 2 } { r config resetstat # Create a large unsorted set collection to ensure it exceeds LAZYFREE_THRESHOLD for {set i 0} {$i < 1024} {incr i} { r sadd myset $i } r keymeta.set [cname $cid] myset "meta" assert_equal [r keymeta.active] 1 r del myset # Wait for lazyfree to complete and verify lazyfreed_objects incremented wait_for_condition 50 100 { [s lazyfree_pending_objects] == 0 } else { fail "lazyfree isn't done" } assert_equal [r keymeta.active] 0 assert_equal [s lazyfreed_objects] 1 } r config set lazyfree-lazy-user-del no } {OK} {needs:config-resetstat} test "KEYMETA - Verify metadata cleanup on expire" { # Class 2 has UNLINKFREE flag, so it should call unlink callback when lazyfree is enabled # Class 1 does not have UNLINKFREE flag, so it should only call free callback foreach {cid} { 1 2 } { r set mykey "mykey$cid" r keymeta.set [cname $cid] mykey "meta" assert_equal [r keymeta.active] 1 r pexpire mykey 1 wait_for_condition 50 100 { [r exists mykey] == 0 } else { fail "key not expired" } assert_equal [r keymeta.active] 0 } } # ============================================================================ # AOF Rewrite Tests # ============================================================================ # Note: Full AOF round-trip tests (write → restart → load) are not included # because the test module registers classes dynamically via commands, which # creates a chicken-and-egg problem: # - Classes must be registered BEFORE AOF loading (in RedisModule_OnLoad) # - But the KEYMETA.REGISTER commands are in the AOF itself # - When server restarts and loads AOF, classes aren't registered yet # - KEYMETA.SET commands fail with "metadata class not found" # # For production modules, classes MUST be registered in RedisModule_OnLoad() # to ensure they're available when AOF/RDB files are loaded on server startup. # See src/module.c documentation for RM_CreateKeyMetaClass() for details. # # The test below verifies that AOF callbacks correctly emit KEYMETA.SET commands # to the AOF file during rewrite, which is the module's responsibility. test "KEYMETA - AOF rewrite emits correct KEYMETA.SET commands to file" { # This test verifies that the AOF callback implementation correctly writes # KEYMETA.SET commands to the AOF file during rewrite. We don't test the # full round-trip (restart + load) due to the dynamic registration limitation # explained above. r config set appendonly yes r config set auto-aof-rewrite-percentage 0 r config set aof-use-rdb-preamble no # Wait for the initial AOF rewrite that Redis triggers when enabling AOF waitForBgrewriteaof r # Create keys with metadata from multiple classes r set key1 "value1" r keymeta.set [cname 1] key1 "metadata_c1" r set key2 "value2" r keymeta.set [cname 2] key2 "metadata_c2" r keymeta.set [cname 3] key2 "metadata_c3" r hset hashkey field1 val1 r keymeta.set [cname 4] hashkey "hash_meta" # Trigger AOF rewrite r bgrewriteaof waitForBgrewriteaof r # Get the AOF directory and read the AOF file set aof_dir [lindex [r config get dir] 1] set aof_base_filename [lindex [r config get appendfilename] 1] # Find the base AOF file (after rewrite) set aof_files [glob -nocomplain -directory $aof_dir appendonlydir/${aof_base_filename}.*.base.aof] assert {[llength $aof_files] > 0} # Read the most recent base AOF file set aof_file [lindex [lsort $aof_files] end] set fp [open $aof_file r] set aof_content [read $fp] close $fp # Verify the AOF contains KEYMETA.SET commands with correct format assert_match "*KEYMETA.SET*[cname 1]*key1*metadata_c1*" $aof_content assert_match "*KEYMETA.SET*[cname 2]*key2*metadata_c2*" $aof_content assert_match "*KEYMETA.SET*[cname 3]*key2*metadata_c3*" $aof_content assert_match "*KEYMETA.SET*[cname 4]*hashkey*hash_meta*" $aof_content # Verify the RESP format is correct by checking for the command structure # The AOF should contain: *4 (array of 4 elements) assert_match "*\$11*KEYMETA.SET*" $aof_content # Count how many KEYMETA.SET commands are in the AOF set keymeta_count [regexp -all {KEYMETA\.SET} $aof_content] assert_equal $keymeta_count 4 } {} {external:skip} # ======================================================================== # RDB Save/Load Tests # ======================================================================== test {RDB: SAVE and reload preserves metadata} { # Create key with metadata r set key1 "value1" r keymeta.set [cname 1] key1 "key1_meta1" assert_equal [r keymeta.get [cname 1] key1] "key1_meta1" r save r debug reload # Verify metadata persisted after reload assert_equal [r keymeta.get [cname 1] key1] "key1_meta1" flushallAndVerifyCleanup } {} {external:skip needs:save} test {RDB: BGSAVE writes metadata to RDB file} { # Create keys with different metadata combinations r set key1 "value1" r keymeta.set [cname 1] key1 "key1_meta1" r set key2 "value2" r keymeta.set [cname 1] key2 "key2_meta1" r keymeta.set [cname 2] key2 "key2_meta2" # Trigger BGSAVE and reload (debug reload preserves modules) r bgsave waitForBgsave r r debug reload # Verify metadata persisted after reload assert_equal [r keymeta.get [cname 1] key1] "key1_meta1" assert_equal [r keymeta.get [cname 1] key2] "key2_meta1" assert_equal [r keymeta.get [cname 2] key2] "key2_meta2" flushallAndVerifyCleanup } {} {external:skip needs:save} test {RDB: Metadata persists with expiretime} { # Create key with both expiry and metadata r set key1 "value1" set expire_time [expr {[clock seconds] + 10000}] r expireat key1 $expire_time r keymeta.set [cname 1] key1 "meta_with_expire" assert_equal [r expiretime key1] $expire_time assert_equal [r keymeta.get [cname 1] key1] "meta_with_expire" # Reload from RDB r debug reload # Verify metadata and expiry persist after reload assert_equal [r expiretime key1] $expire_time assert_equal [r keymeta.get [cname 1] key1] "meta_with_expire" flushallAndVerifyCleanup } {} {external:skip needs:debug} test {RDB: Create keys with upto 7 meta classes, with or without expiry} { # Test all combinations of 1-7 metadata classes, with or without expiry for {set n 1} {$n <= 7} {incr n} { foreach hasExpiry {0 1} { set keyname "key_${n}_exp${hasExpiry}" r set $keyname "value$n" # Set expiry if hasExpiry is 1 if {$hasExpiry} { set ttl [expr {3600 + $n}] r expire $keyname $ttl # Get the actual expiretime set by Redis to use as expected value set expExpiry [r expiretime $keyname] } # Create list of class IDs to attach (1 through n) set class_ids {} for {set i 1} {$i <= $n} {incr i} { lappend class_ids $i } # Randomize the order of metadata attachment set class_ids [lshuffle $class_ids] # Attach metadata in randomized order foreach cid $class_ids { r keymeta.set [cname $cid] $keyname "meta$cid" } # Verify metadata before RDB save # Verify exactly n metadata classes are attached for {set i 1} {$i <= 7} {incr i} { if {$i <= $n} { assert_equal [r keymeta.get [cname $i] $keyname] "meta$i" } else { assert_equal [r keymeta.get [cname $i] $keyname] "" } } # Verify expiry before RDB save if {$hasExpiry} { set actual_expiretime [r expiretime $keyname] assert_equal $actual_expiretime $expExpiry } # Save and reload from RDB (debug reload preserves modules) r save r debug reload # Verify metadata after RDB reload # Verify exactly n metadata classes are still attached for {set i 1} {$i <= 7} {incr i} { if {$i <= $n} { assert_equal [r keymeta.get [cname $i] $keyname] "meta$i" } else { assert_equal [r keymeta.get [cname $i] $keyname] "" } } # Verify expiry after RDB reload if {$hasExpiry} { set actual_expiretime [r expiretime $keyname] assert_equal $actual_expiretime $expExpiry } else { # Verify no expiry set assert_equal [r expiretime $keyname] -1 } flushallAndVerifyCleanup } } } {} {external:skip needs:save} # ======================================================================== # RDB Flag Tests: ALLOW_IGNORE, RDBLOAD, RDBSAVE # ======================================================================== # Test all combinations except the error case (ALLOW_IGNORE=0, RDBLOAD=0, RDBSAVE=1) foreach RDBLOAD {0 1} { foreach RDBSAVE {0 1} { foreach ALLOW_IGNORE {0 1} { # Skip the error case - we'll test it last since it causes RDB load to fail if {!$RDBLOAD && $RDBSAVE && !$ALLOW_IGNORE} { continue } test "RDB: SAVE and LOAD (ALLOW_IGNORE=$ALLOW_IGNORE, RDBLOAD=$RDBLOAD, RDBSAVE=$RDBSAVE)" { # Flush all data and save empty RDB to start with a clean slate r flushall r save # re-register class 1 with new flags. Expected re-registered same class ID r keymeta.unregister [cname 1] # dummy default spec set newSpec "KEEPONCOPY" if {$ALLOW_IGNORE} { append newSpec ":ALLOWIGNORE" } if {$RDBLOAD} { append newSpec ":RDBLOAD" } if {$RDBSAVE} { append newSpec ":RDBSAVE" } # Must reuse same class-id that it had before assert_equal $classes(1) [r keymeta.register [cname 1] 1 $newSpec] r set key1 "value1" r keymeta.set [cname 1] key1 "key1_meta1" assert_equal [r keymeta.get [cname 1] key1] "key1_meta1" r save r debug reload # Metadata is preserved only when BOTH rdb_save AND rdb_load are enabled # Otherwise metadata is lost (either not saved, or saved but not loaded) set metaPreserved [expr {$RDBSAVE && $RDBLOAD}] set expectedMeta [expr {$metaPreserved ? "key1_meta1" : ""}] assert_equal [r keymeta.get [cname 1] key1] $expectedMeta flushallAndVerifyCleanup } {} {external:skip needs:save} } } } # Test the error case last (ALLOW_IGNORE=0, RDBLOAD=0, RDBSAVE=1) # This test causes RDB load to fail, so we test it last to avoid polluting subsequent tests test "RDB: SAVE and LOAD Invalid combination: (ALLOW_IGNORE=0, RDBLOAD=0, RDBSAVE=1)" { # re-register class 1 with RDBSAVE flag but no RDBLOAD or ALLOW_IGNORE r keymeta.unregister [cname 1] set newSpec "KEEPONCOPY:RDBSAVE" assert_equal $classes(1) [r keymeta.register [cname 1] 1 $newSpec] r set key1 "value1" r keymeta.set [cname 1] key1 "key1_meta1" assert_equal [r keymeta.get [cname 1] key1] "key1_meta1" r save # This combination causes RDB load to fail because: # - Metadata was saved (RDBSAVE=1) # - Class has no rdb_load callback (RDBLOAD=0) # - Errors are not ignored (ALLOW_IGNORE=0) catch {r debug reload} err assert_match "*Error trying to load the RDB dump*" $err } {} {external:skip needs:save} # ======================================================================== # DUMP/RESTORE Tests # ======================================================================== test {DUMP/RESTORE: 1 to 7 metadata classes, optional TTL} { foreach withTTL {0 1} { for {set numClasses 1} {$numClasses < 8} {incr numClasses} { # Re-register classes with RDBLOAD and RDBSAVE flags for {set cid 1} {$cid <= $numClasses} {incr cid} { r keymeta.unregister [cname $cid] assert_equal $classes($cid) [r keymeta.register [cname $cid] 1 $classesSpec($cid)] } # Create key with metadata classes r set key1 "value1" for {set i 1} {$i <= $numClasses} {incr i} { r keymeta.set [cname $i] key1 "meta${i}_value" } if {$withTTL} { r expire key1 10000 } # Verify all metadata before DUMP for {set i 1} {$i <= $numClasses} {incr i} { assert_equal [r keymeta.get [cname $i] key1] "meta${i}_value" } # DUMP the key set encoded [r dump key1] # Delete and RESTORE r del key1 r restore key1 [expr {$withTTL ? 10000 : 0}] $encoded # Verify all metadata was restored assert_equal [r get key1] "value1" for {set i 1} {$i <= $numClasses} {incr i} { assert_equal [r keymeta.get [cname $i] key1] "meta${i}_value" } if {$withTTL} { assert_range [r pttl key1] 9000 10000 } flushallAndVerifyCleanup } } } test {DUMP/RESTORE: REPLACE with metadata} { # Create key with metadata r set key1 value1 r keymeta.set [cname 1] key1 "meta1_original" # DUMP the key set encoded1 [r dump key1] # Create different key with different metadata r set key1 value2 r keymeta.set [cname 1] key1 "meta1_new" # DUMP the second version set encoded2 [r dump key1] # Delete and restore first version r del key1 r restore key1 0 $encoded1 assert_equal [r get key1] "value1" assert_equal [r keymeta.get [cname 1] key1] "meta1_original" # RESTORE second version with REPLACE r restore key1 0 $encoded2 replace assert_equal [r get key1] "value2" assert_equal [r keymeta.get [cname 1] key1] "meta1_new" flushallAndVerifyCleanup } # Test all combinations except the error case (ALLOW_IGNORE=0, RDBLOAD=0, RDBSAVE=1) foreach RDBLOAD {0 1} { foreach RDBSAVE {0 1} { foreach ALLOW_IGNORE {0 1} { # Skip the error case - we'll test it last since it causes RESTORE to fail if {!$RDBLOAD && $RDBSAVE && !$ALLOW_IGNORE} { continue } test "DUMP/RESTORE: (ALLOW_IGNORE=$ALLOW_IGNORE, RDBLOAD=$RDBLOAD, RDBSAVE=$RDBSAVE)" { # re-register class 1 with new flags. Expected re-registered same class ID r keymeta.unregister [cname 1] # dummy default spec set newSpec "KEEPONCOPY" if {$ALLOW_IGNORE} { append newSpec ":ALLOWIGNORE" } if {$RDBLOAD} { append newSpec ":RDBLOAD" } if {$RDBSAVE} { append newSpec ":RDBSAVE" } # Must reuse same class-id that it had before assert_equal $classes(1) [r keymeta.register [cname 1] 1 $newSpec] r set key1 "value1" r keymeta.set [cname 1] key1 "key1_meta1" assert_equal [r keymeta.get [cname 1] key1] "key1_meta1" # DUMP & RESTORE set encoded [r dump key1] r del key1 r restore key1 0 $encoded # Metadata is preserved only when BOTH rdb_save AND rdb_load are enabled # Otherwise metadata is lost (either not saved, or saved but not loaded) set metaPreserved [expr {$RDBSAVE && $RDBLOAD}] set expectedMeta [expr {$metaPreserved ? "key1_meta1" : ""}] assert_equal [r keymeta.get [cname 1] key1] $expectedMeta flushallAndVerifyCleanup } } } } # Test the error case last (ALLOW_IGNORE=0, RDBLOAD=0, RDBSAVE=1) # This test causes RESTORE to fail, so we test it last to avoid polluting subsequent tests test "DUMP/RESTORE: Invalid combination: (ALLOW_IGNORE=0, RDBLOAD=0, RDBSAVE=1)" { # re-register class 1 with RDBSAVE flag but no RDBLOAD or ALLOW_IGNORE r keymeta.unregister [cname 1] set newSpec "KEEPONCOPY:RDBSAVE" assert_equal $classes(1) [r keymeta.register [cname 1] 1 $newSpec] r set key1 "value1" r keymeta.set [cname 1] key1 "key1_meta1" assert_equal [r keymeta.get [cname 1] key1] "key1_meta1" # DUMP the key set encoded [r dump key1] # Delete and try to RESTORE r del key1 # This combination causes RESTORE to fail because: # - Metadata was saved (RDBSAVE=1) # - Class has no rdb_load callback (RDBLOAD=0) # - Errors are not ignored (ALLOW_IGNORE=0) catch {r restore key1 0 $encoded} err assert_match "*Bad data format*" $err flushallAndVerifyCleanup } } test "RDB: Load with different module registration order preserves metadata correctly" { # This test verifies out-of-order metadata attachment during RDB load. # When modules register in different order at load time vs save time, # metadata values should still be correctly associated with their classes. start_server {tags {"modules" "external:skip" "cluster:skip"} overrides {enable-debug-command yes}} { r module load $testmodule # Helper function to generate class names (needed in inner scope) proc cname {id} { return "CLS$id" } # Register classes in order: 1, 2, 3 set spec1 "KEEPONCOPY:ALLOWIGNORE:RDBLOAD:RDBSAVE" set spec2 "KEEPONRENAME:ALLOWIGNORE:RDBLOAD:RDBSAVE" set spec3 "KEEPONMOVE:ALLOWIGNORE:RDBLOAD:RDBSAVE" set class1 [r keymeta.register [cname 1] 1 $spec1] set class2 [r keymeta.register [cname 2] 1 $spec2] set class3 [r keymeta.register [cname 3] 1 $spec3] # Verify class IDs match registration order assert_equal $class1 1 "Class 1 registered first, gets ID 1" assert_equal $class2 2 "Class 2 registered second, gets ID 2" assert_equal $class3 3 "Class 3 registered third, gets ID 3" # OUTER SERVER: Create RDB with classes registered in order 1,2,3 r flushall r set mykey "myvalue" r keymeta.set [cname 1] mykey "metadata_for_class1" r keymeta.set [cname 2] mykey "metadata_for_class2" r keymeta.set [cname 3] mykey "metadata_for_class3" # Verify metadata before save assert_equal [r keymeta.get [cname 1] mykey] "metadata_for_class1" assert_equal [r keymeta.get [cname 2] mykey] "metadata_for_class2" assert_equal [r keymeta.get [cname 3] mykey] "metadata_for_class3" r save # Get RDB file path & Copy RDB to a temp location with unique name set rdb_dir [lindex [r config get dir] 1] set rdb_file [lindex [r config get dbfilename] 1] set rdb_path [file join $rdb_dir $rdb_file] set temp_rdb [file join $rdb_dir "temp_metadata_outoforder_[pid].rdb"] file copy -force $rdb_path $temp_rdb # INNER SERVER: Start new server, register classes in DIFFERENT order, then load RDB start_server [list overrides [list dir $rdb_dir enable-debug-command yes]] { r module load $testmodule # Helper function to generate class names (needed in inner scope) proc cname {id} { return "CLS$id" } # Register classes in DIFFERENT order: 3, 1, 2 # This simulates a server where modules load in different order set spec1 "KEEPONCOPY:ALLOWIGNORE:RDBLOAD:RDBSAVE" set spec2 "KEEPONRENAME:ALLOWIGNORE:RDBLOAD:RDBSAVE" set spec3 "KEEPONMOVE:ALLOWIGNORE:RDBLOAD:RDBSAVE" set class3 [r keymeta.register [cname 3] 1 $spec3] set class1 [r keymeta.register [cname 1] 1 $spec1] set class2 [r keymeta.register [cname 2] 1 $spec2] # Verify class IDs are assigned by REGISTRATION ORDER, not name # We registered in order 3,1,2, so the runtime IDs are: # - class3 (name "CLS3") gets ID 1 (first registered) # - class1 (name "CLS1") gets ID 2 (second registered) # - class2 (name "CLS2") gets ID 3 (third registered) # This is DIFFERENT from outer server which registered in order 1,2,3 assert_equal $class3 1 "Class 3 registered first, gets ID 1" assert_equal $class1 2 "Class 1 registered second, gets ID 2" assert_equal $class2 3 "Class 2 registered third, gets ID 3" # Copy the saved RDB to this server's dbfilename set inner_rdb_file [lindex [r config get dbfilename] 1] set inner_rdb_path [file join $rdb_dir $inner_rdb_file] file copy -force $temp_rdb $inner_rdb_path # NOW load the RDB (AFTER registration in different order) # Use 'nosave' to reload from the copied RDB without saving current state first r debug reload nosave # Verify the key exists assert_equal [r exists mykey] 1 "Key should exist after RDB load" assert_equal [r get mykey] "myvalue" "Key value should be preserved" # Verify metadata values are correctly associated with their classes # WITHOUT metadata would be swapped because: # - At SAVE time (outer): classes registered in order 1,2,3 # - At LOAD time (inner): classes registered in order 3,1,2 # - RDB contains metadata in saved order, but keyMetaClassLookupByName # maps them back to correct classes by NAME, not by registration order assert_equal [r keymeta.get [cname 1] mykey] "metadata_for_class1" assert_equal [r keymeta.get [cname 2] mykey] "metadata_for_class2" assert_equal [r keymeta.get [cname 3] mykey] "metadata_for_class3" } # Cleanup temp file file delete $temp_rdb } } {} {external:skip needs:save} test "RDB: File size same with/without metadata when no rdb_save callback" { # This test verifies that when a metadata class has no rdb_save callback, # the metadata is not serialized to RDB, so the RDB file size should be # approximately the same (within a small tolerance for header differences). start_server {tags {"modules" "external:skip" "cluster:skip"} overrides {enable-debug-command yes}} { r module load $testmodule # Get RDB directory set rdb_dir [lindex [r config get dir] 1] set rdb_file [lindex [r config get dbfilename] 1] set rdb_path [file join $rdb_dir $rdb_file] # Test 1: Create key WITHOUT metadata and save r flushall r set key1 "test_value_12345" r save set size_without_meta [file size $rdb_path] # Test 2: Create identical key WITH metadata (but no rdb_save) and save # Register a class WITHOUT rdb_save callback (RDBSAVE=0) # Use ALLOWIGNORE so loading doesn't fail when metadata is missing set spec "ALLOWIGNORE" r keymeta.register [cname 1] 1 $spec r flushall r set key1 "test_value_12345" r keymeta.set [cname 1] key1 "some_metadata_value" # Verify metadata is attached assert_equal [r keymeta.get [cname 1] key1] "some_metadata_value" r save set size_with_meta [file size $rdb_path] # The file sizes should be the same (metadata not serialized) assert_equal $size_without_meta $size_with_meta } } {} {external:skip needs:save} test "Creating key metadata not during OnLoad should fail" { # This time start_server without "enable-debug-command yes" start_server {tags {"modules" "external:skip" "cluster:skip"} overrides {enable-debug-command no}} { r module load $testmodule # Creating a class not during OnLoad should fail catch {r keymeta.register [cname 1] 1 "ALLOWIGNORE"} err assert_match {*failed to create metadata class*} $err } } {} {external:skip needs:save}