mirror of
https://github.com/redis/redis.git
synced 2026-05-27 11:43:04 -04:00
M_CreateKeyMetaClass() allows registration only on: - 'DEBUG enable-module-keymeta-runtime-registration 1' (replaces server.enable_debug_cmd) - REDISMODULE_CTX_FLAGS_SERVER_STARTUP, in addition to module->onload
914 lines
38 KiB
Tcl
914 lines
38 KiB
Tcl
# ============================================================================
|
|
# 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
|
|
r debug enable-keymeta-runtime-registration 1
|
|
|
|
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
|
|
r debug enable-keymeta-runtime-registration 1
|
|
|
|
# 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
|
|
r debug enable-keymeta-runtime-registration 1
|
|
|
|
# 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
|
|
r debug enable-keymeta-runtime-registration 1
|
|
|
|
# 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" {
|
|
# Start server without enabling keymeta runtime registration debug flag
|
|
start_server {tags {"modules" "external:skip" "cluster:skip"} overrides {enable-debug-command no}} {
|
|
r module load $testmodule
|
|
# Creating a class not during server startup should fail
|
|
catch {r keymeta.register [cname 1] 1 "ALLOWIGNORE"} err
|
|
assert_match {*failed to create metadata class*} $err
|
|
}
|
|
} {} {external:skip needs:save}
|