summaryrefslogtreecommitdiff
path: root/examples/redis-unstable/tests/unit/moduleapi/keymeta.tcl
diff options
context:
space:
mode:
authorMitja Felicijan <mitja.felicijan@gmail.com>2026-01-21 22:52:54 +0100
committerMitja Felicijan <mitja.felicijan@gmail.com>2026-01-21 22:52:54 +0100
commitdcacc00e3750300617ba6e16eb346713f91a783a (patch)
tree38e2d4fb5ed9d119711d4295c6eda4b014af73fd /examples/redis-unstable/tests/unit/moduleapi/keymeta.tcl
parent58dac10aeb8f5a041c46bddbeaf4c7966a99b998 (diff)
downloadcrep-dcacc00e3750300617ba6e16eb346713f91a783a.tar.gz
Remove testing data
Diffstat (limited to 'examples/redis-unstable/tests/unit/moduleapi/keymeta.tcl')
-rw-r--r--examples/redis-unstable/tests/unit/moduleapi/keymeta.tcl910
1 files changed, 0 insertions, 910 deletions
diff --git a/examples/redis-unstable/tests/unit/moduleapi/keymeta.tcl b/examples/redis-unstable/tests/unit/moduleapi/keymeta.tcl
deleted file mode 100644
index ebb7784..0000000
--- a/examples/redis-unstable/tests/unit/moduleapi/keymeta.tcl
+++ /dev/null
@@ -1,910 +0,0 @@
1# ============================================================================
2# Key Metadata (keymeta) Test Suite
3# ============================================================================
4#
5# Tests the Redis module key metadata framework: up to 7 independent metadata
6# classes (IDs 1-7) can be attached to keys. Class ID 0 is reserved for key
7# expiration.
8#
9# The following features are sensitive to Key Metadata and are tested here:
10#
11# - KEY EXPIRATION (class ID 0)
12# - Stored at ((uint64_t *)kv) - 1 (first metadata slot)
13# - Managed via db->expires dictionary
14# - Must be preserved/updated when kvobj is reallocated
15# - HASH FIELD EXPIRATION (HFE)
16# - NOT in kvobj metadata slots (Maybe in the future...)
17# - Managed via db->hexpires ebuckets (holds direct kvobj pointer)
18# - Must be removed before kvobj reallocation (hashTypeRemoveFromExpires)
19# and restored after (hashTypeAddToExpires)
20# - MODULE METADATA (class IDs 1-7)
21# - Defines metadata lifecycle via callbacks
22# - EMBEDDED STRINGS vs. REGULAR OBJECTS
23# - Short strings and numbers are embedded into kvobj
24# - The rest are kept as distinct objects
25# - LAZYFREE
26# ============================================================================
27
28set testmodule [file normalize tests/modules/test_keymeta.so]
29
30# Helper procedure to convert class ID to 4-char-id name
31proc cname {cid} {
32 return "KMT$cid"
33}
34
35# Helper procedure to check if a class should keep metadata for a given operation
36proc shouldKeep {cid operation classesSpec} {
37 upvar $classesSpec specs
38 set spec $specs($cid)
39 switch $operation {
40 "copy" { return [string match "*KEEPONCOPY*" $spec] }
41 "rename" { return [string match "*KEEPONRENAME*" $spec] }
42 "move" { return [string match "*KEEPONMOVE*" $spec] }
43 default { return 0 }
44 }
45}
46
47# Helper procedure to setup a key with metadata
48proc setupKeyMeta {keyname numClasses expiryBefore expiryAfter} {
49 # Set expiry if requested
50 if {$expiryBefore} {
51 r expire $keyname 10000
52 assert_range [r ttl $keyname] 9990 10000
53 }
54
55 # Set metadata for all classes
56 for {set i 1} {$i <= $numClasses} {incr i} {
57 # Set twice to verify overwrite behavior
58 r keymeta.set [cname $i] $keyname "blabla$i"
59 assert_equal [r keymeta.get [cname $i] $keyname] "blabla$i"
60 r keymeta.set [cname $i] $keyname "meta$i"
61 }
62
63 # Verify metadata was set correctly
64 for {set i 1} {$i <= $numClasses} {incr i} {
65 assert_equal [r keymeta.get [cname $i] $keyname] "meta$i"
66 }
67
68 if {$expiryAfter} {
69 r expire $keyname 10000
70 assert_range [r ttl $keyname] 9990 10000
71 }
72
73 if {$expiryBefore} {
74 assert_range [r ttl $keyname] 9990 10000
75 }
76}
77
78# Helper procedure to verify metadata after an operation
79proc verifyKeyMeta {keyname operation numClasses hasExpiry classesSpec} {
80 upvar $classesSpec specs
81
82 # Verify expiry
83 if {$hasExpiry} {
84 assert_range [r ttl $keyname] 9990 10000
85 }
86
87 # Verify metadata based on class spec
88 for {set i 1} {$i <= $numClasses} {incr i} {
89 set expected [expr {[shouldKeep $i $operation specs] ? "meta$i" : ""}]
90 assert_equal [r keymeta.get [cname $i] $keyname] $expected
91 }
92}
93
94proc flushallAndVerifyCleanup {} {
95 r flushall
96 # Verify all metadata is cleaned up properly
97 assert_equal [r keymeta.active] 0
98}
99
100start_server {tags {"modules" "external:skip" "cluster:skip"} overrides {enable-debug-command yes}} {
101 r module load $testmodule
102
103 array set classesSpec {}
104 set classesSpec(1) "KEEPONCOPY:KEEPONRENAME:KEEPONMOVE:ALLOWIGNORE:RDBLOAD:RDBSAVE"
105 set classesSpec(2) "KEEPONCOPY:KEEPONRENAME:UNLINKFREE:ALLOWIGNORE:RDBLOAD:RDBSAVE"
106 set classesSpec(3) "KEEPONCOPY:ALLOWIGNORE:RDBLOAD:RDBSAVE"
107 set classesSpec(4) "ALLOWIGNORE:RDBLOAD:RDBSAVE"
108 set classesSpec(5) "KEEPONRENAME:KEEPONMOVE:ALLOWIGNORE:RDBLOAD:RDBSAVE"
109 set classesSpec(6) "KEEPONRENAME:ALLOWIGNORE:RDBLOAD:RDBSAVE"
110 set classesSpec(7) "KEEPONMOVE:UNLINKFREE:ALLOWIGNORE:RDBLOAD:RDBSAVE"
111
112 array set classes {}
113 for {set cid 1} {$cid <= 7} {incr cid} {
114 set spec $classesSpec($cid)
115 set classes($cid) [r keymeta.register [cname $cid] 1 $spec]
116 puts "Registered class $cid with spec $spec"
117 assert_equal $classes($cid) $cid
118 }
119
120 # Validates metadata behavior across COPY/RENAME/MOVE operations
121 # with varying numbers of metadata classes (1-7), key expiration states,
122 # key types (string/hash), hash field expiration, and metadata class flags
123 # (KEEPONCOPY, KEEPONRENAME, KEEPONMOVE).
124 for {set numClasses 1} {$numClasses < 8} {incr numClasses} {
125 foreach expiryBefore {0 1} {
126 foreach expiryAfter {0 1} {
127 set hasExpiry [expr {$expiryBefore || $expiryAfter}]
128 set expiryStr "expiryBefore=$expiryBefore, expiryAfter=$expiryAfter)"
129 # Test COPY operation
130 test "KEYMETA - copy key-string with $numClasses classes, $expiryStr" {
131 foreach value { 3 "value1" [string repeat "ABCD" 1000]} {
132 r select 0
133 r del k1 k2
134 r set k1 $value
135 setupKeyMeta k1 $numClasses $expiryBefore $expiryAfter
136 # Copy:
137 r copy k1 k2
138 # Verify:
139 assert_equal [r get k1] $value
140 assert_equal [r get k2] $value
141 # Verify expiry and metadata
142 verifyKeyMeta k2 "copy" $numClasses $hasExpiry classesSpec
143 flushallAndVerifyCleanup
144 }
145 }
146
147 test "KEYMETA - copy key-hash with $numClasses classes, $expiryStr" {
148 r select 0
149 r del h1 h2
150 r HSET h1 field1 "value1" field2 "value2"
151 r hexpire h1 10000 FIELDS 1 field1
152 setupKeyMeta h1 $numClasses $expiryBefore $expiryAfter
153 # Copy:
154 r copy h1 h2
155 # Verify:
156 verifyKeyMeta h2 "copy" $numClasses $hasExpiry classesSpec
157 assert_range [r httl h1 FIELDS 1 field1] 9999 10000
158 assert_range [r httl h2 FIELDS 1 field1] 9999 10000
159 flushallAndVerifyCleanup
160 }
161
162 # Test RENAME operation
163 test "KEYMETA - rename key-string with $numClasses classes, $expiryStr" {
164 foreach value { 3 "value1" [string repeat "ABCD" 1000]} {
165 r select 0
166 r del k1 k2
167 r set k1 $value
168 setupKeyMeta k1 $numClasses $expiryBefore $expiryAfter
169 # Rename:
170 r rename k1 k2
171 # Verify:
172 assert_equal [r exists k1] 0
173 assert_equal [r get k2] $value
174 # Verify expiry and metadata
175 verifyKeyMeta k2 "rename" $numClasses $hasExpiry classesSpec
176 flushallAndVerifyCleanup
177 }
178 }
179
180 test "KEYMETA - rename key-hash with $numClasses classes, $expiryStr" {
181 r select 0
182 r del h1 h2
183 r HSET h1 field1 "value1" field2 "value2"
184 r hexpire h1 10000 FIELDS 1 field1
185 setupKeyMeta h1 $numClasses $expiryBefore $expiryAfter
186 # Rename:
187 r rename h1 h2
188 # Verify:
189 assert_equal [r exists h1] 0
190 assert_range [r httl h2 FIELDS 1 field1] 9999 10000
191 verifyKeyMeta h2 "rename" $numClasses $hasExpiry classesSpec
192 flushallAndVerifyCleanup
193 }
194
195
196
197 # Test MOVE operation
198 test "KEYMETA - move key-string with $numClasses classes, $expiryStr" {
199 foreach value { 3 "value1" [string repeat "ABCD" 1000]} {
200 r select 9
201 r del k1
202 r select 0
203 r del k1
204 r set k1 $value
205 setupKeyMeta k1 $numClasses $expiryBefore $expiryAfter
206 # Perform move
207 assert_equal [r move k1 9] 1
208 # Verify key moved
209 assert_equal [r exists k1] 0
210 r select 9
211 assert_equal [r get k1] $value
212 # Verify expiry and metadata
213 verifyKeyMeta k1 "move" $numClasses $hasExpiry classesSpec
214 r select 0
215 flushallAndVerifyCleanup
216 }
217 }
218
219 test "KEYMETA - move key-hash with $numClasses classes, $expiryStr" {
220 r select 9
221 r del h1
222 r select 0
223 r del h1
224 r HSET h1 field1 "value1" field2 "value2"
225 r hexpire h1 10000 FIELDS 1 field1
226 setupKeyMeta h1 $numClasses $expiryBefore $expiryAfter
227 assert_range [r httl h1 FIELDS 1 field1] 9999 10000
228 assert_equal [r move h1 9] 1
229 assert_equal [r exists h1] 0
230 r select 9
231 assert_range [r httl h1 FIELDS 1 field1] 9999 10000
232 verifyKeyMeta h1 "move" $numClasses $hasExpiry classesSpec
233 r select 0
234 flushallAndVerifyCleanup
235 }
236 }
237 }
238 }
239
240 test "KEYMETA - Verify active metadata count on copy" {
241 for {set cid 1} {$cid < 7} {incr cid} {
242 set numAlloc 0
243 flushallAndVerifyCleanup
244 set dupOnCopy [shouldKeep $cid "copy" classesSpec]
245 r set k1 "v1"
246 r keymeta.set [cname $cid] k1 "meta1"
247 assert_equal [r keymeta.active] [incr numAlloc]
248 r keymeta.set [cname $cid] k1 "meta1b"
249 assert_equal [r keymeta.active] $numAlloc
250 r copy k1 k1copy
251 assert_equal [r keymeta.active] [incr numAlloc $dupOnCopy]
252 r del k1
253 assert_equal [r keymeta.active] [incr numAlloc -1]
254 r del k1copy
255 assert_equal [r keymeta.active] 0
256 }
257 }
258
259 test "KEYMETA - Verify active metadata count on rename" {
260 for {set cid 1} {$cid <= 7} {incr cid} {
261 set numAlloc 0
262 flushallAndVerifyCleanup
263 set keepOnRename [shouldKeep $cid "rename" classesSpec]
264 set discOnRename [expr {!$keepOnRename}]
265 r set k1 "v1"
266 r keymeta.set [cname $cid] k1 "meta1"
267 assert_equal [r keymeta.active] [incr numAlloc]
268 r rename k1 k1_renamed
269 assert_equal [r keymeta.active] [incr numAlloc -$discOnRename]
270 r del k1_renamed
271 assert_equal [r keymeta.active] 0
272 }
273 }
274
275 test "KEYMETA - Verify active metadata count on move" {
276 for {set cid 1} {$cid <= 7} {incr cid} {
277 set numAlloc 0
278 r select 0
279 flushallAndVerifyCleanup
280
281 set keepOnMove [shouldKeep $cid "move" classesSpec]
282 set discOnMove [expr {!$keepOnMove}]
283
284 # Create keys with metadata in DB 0
285 r set k1 "v1"
286 r keymeta.set [cname $cid] k1 "meta1"
287 assert_equal [r keymeta.active] [incr numAlloc]
288 # Move: metadata discarded if !keepOnMove
289 r move k1 9
290 set active [r keymeta.active]
291 assert_equal [r keymeta.active] [incr numAlloc -$discOnMove]
292 # Cleanup
293 r select 9
294 r del k1
295 r select 0
296 assert_equal [r keymeta.active] 0
297 }
298 }
299
300 test "KEYMETA - Verify metadta cleanup on lazyfree" {
301 r config set lazyfree-lazy-user-del yes
302 # Class 2 has UNLINKFREE flag, so it should call unlink callback when lazyfree is enabled
303 # Class 1 does not have UNLINKFREE flag, so it should only call free callback
304 foreach {cid} { 1 2 } {
305 r config resetstat
306 # Create a large unsorted set collection to ensure it exceeds LAZYFREE_THRESHOLD
307 for {set i 0} {$i < 1024} {incr i} { r sadd myset $i }
308 r keymeta.set [cname $cid] myset "meta"
309 assert_equal [r keymeta.active] 1
310 r del myset
311
312 # Wait for lazyfree to complete and verify lazyfreed_objects incremented
313 wait_for_condition 50 100 {
314 [s lazyfree_pending_objects] == 0
315 } else {
316 fail "lazyfree isn't done"
317 }
318 assert_equal [r keymeta.active] 0
319 assert_equal [s lazyfreed_objects] 1
320 }
321 r config set lazyfree-lazy-user-del no
322 } {OK} {needs:config-resetstat}
323
324 test "KEYMETA - Verify metadata cleanup on expire" {
325 # Class 2 has UNLINKFREE flag, so it should call unlink callback when lazyfree is enabled
326 # Class 1 does not have UNLINKFREE flag, so it should only call free callback
327 foreach {cid} { 1 2 } {
328 r set mykey "mykey$cid"
329 r keymeta.set [cname $cid] mykey "meta"
330 assert_equal [r keymeta.active] 1
331 r pexpire mykey 1
332 wait_for_condition 50 100 {
333 [r exists mykey] == 0
334 } else {
335 fail "key not expired"
336 }
337 assert_equal [r keymeta.active] 0
338 }
339 }
340
341 # ============================================================================
342 # AOF Rewrite Tests
343 # ============================================================================
344 # Note: Full AOF round-trip tests (write → restart → load) are not included
345 # because the test module registers classes dynamically via commands, which
346 # creates a chicken-and-egg problem:
347 # - Classes must be registered BEFORE AOF loading (in RedisModule_OnLoad)
348 # - But the KEYMETA.REGISTER commands are in the AOF itself
349 # - When server restarts and loads AOF, classes aren't registered yet
350 # - KEYMETA.SET commands fail with "metadata class not found"
351 #
352 # For production modules, classes MUST be registered in RedisModule_OnLoad()
353 # to ensure they're available when AOF/RDB files are loaded on server startup.
354 # See src/module.c documentation for RM_CreateKeyMetaClass() for details.
355 #
356 # The test below verifies that AOF callbacks correctly emit KEYMETA.SET commands
357 # to the AOF file during rewrite, which is the module's responsibility.
358 test "KEYMETA - AOF rewrite emits correct KEYMETA.SET commands to file" {
359 # This test verifies that the AOF callback implementation correctly writes
360 # KEYMETA.SET commands to the AOF file during rewrite. We don't test the
361 # full round-trip (restart + load) due to the dynamic registration limitation
362 # explained above.
363
364 r config set appendonly yes
365 r config set auto-aof-rewrite-percentage 0
366 r config set aof-use-rdb-preamble no
367 # Wait for the initial AOF rewrite that Redis triggers when enabling AOF
368 waitForBgrewriteaof r
369
370 # Create keys with metadata from multiple classes
371 r set key1 "value1"
372 r keymeta.set [cname 1] key1 "metadata_c1"
373
374 r set key2 "value2"
375 r keymeta.set [cname 2] key2 "metadata_c2"
376 r keymeta.set [cname 3] key2 "metadata_c3"
377
378 r hset hashkey field1 val1
379 r keymeta.set [cname 4] hashkey "hash_meta"
380
381 # Trigger AOF rewrite
382 r bgrewriteaof
383 waitForBgrewriteaof r
384
385 # Get the AOF directory and read the AOF file
386 set aof_dir [lindex [r config get dir] 1]
387 set aof_base_filename [lindex [r config get appendfilename] 1]
388
389 # Find the base AOF file (after rewrite)
390 set aof_files [glob -nocomplain -directory $aof_dir appendonlydir/${aof_base_filename}.*.base.aof]
391 assert {[llength $aof_files] > 0}
392
393 # Read the most recent base AOF file
394 set aof_file [lindex [lsort $aof_files] end]
395 set fp [open $aof_file r]
396 set aof_content [read $fp]
397 close $fp
398
399 # Verify the AOF contains KEYMETA.SET commands with correct format
400 assert_match "*KEYMETA.SET*[cname 1]*key1*metadata_c1*" $aof_content
401 assert_match "*KEYMETA.SET*[cname 2]*key2*metadata_c2*" $aof_content
402 assert_match "*KEYMETA.SET*[cname 3]*key2*metadata_c3*" $aof_content
403 assert_match "*KEYMETA.SET*[cname 4]*hashkey*hash_meta*" $aof_content
404
405 # Verify the RESP format is correct by checking for the command structure
406 # The AOF should contain: *4 (array of 4 elements)
407 assert_match "*\$11*KEYMETA.SET*" $aof_content
408 # Count how many KEYMETA.SET commands are in the AOF
409 set keymeta_count [regexp -all {KEYMETA\.SET} $aof_content]
410 assert_equal $keymeta_count 4
411 } {} {external:skip}
412
413 # ========================================================================
414 # RDB Save/Load Tests
415 # ========================================================================
416
417 test {RDB: SAVE and reload preserves metadata} {
418 # Create key with metadata
419 r set key1 "value1"
420 r keymeta.set [cname 1] key1 "key1_meta1"
421 assert_equal [r keymeta.get [cname 1] key1] "key1_meta1"
422
423 r save
424 r debug reload
425
426 # Verify metadata persisted after reload
427 assert_equal [r keymeta.get [cname 1] key1] "key1_meta1"
428
429 flushallAndVerifyCleanup
430 } {} {external:skip needs:save}
431
432 test {RDB: BGSAVE writes metadata to RDB file} {
433 # Create keys with different metadata combinations
434 r set key1 "value1"
435 r keymeta.set [cname 1] key1 "key1_meta1"
436
437 r set key2 "value2"
438 r keymeta.set [cname 1] key2 "key2_meta1"
439 r keymeta.set [cname 2] key2 "key2_meta2"
440
441 # Trigger BGSAVE and reload (debug reload preserves modules)
442 r bgsave
443 waitForBgsave r
444 r debug reload
445
446 # Verify metadata persisted after reload
447 assert_equal [r keymeta.get [cname 1] key1] "key1_meta1"
448 assert_equal [r keymeta.get [cname 1] key2] "key2_meta1"
449 assert_equal [r keymeta.get [cname 2] key2] "key2_meta2"
450
451 flushallAndVerifyCleanup
452 } {} {external:skip needs:save}
453
454 test {RDB: Metadata persists with expiretime} {
455 # Create key with both expiry and metadata
456 r set key1 "value1"
457 set expire_time [expr {[clock seconds] + 10000}]
458 r expireat key1 $expire_time
459 r keymeta.set [cname 1] key1 "meta_with_expire"
460
461 assert_equal [r expiretime key1] $expire_time
462 assert_equal [r keymeta.get [cname 1] key1] "meta_with_expire"
463
464 # Reload from RDB
465 r debug reload
466
467 # Verify metadata and expiry persist after reload
468 assert_equal [r expiretime key1] $expire_time
469 assert_equal [r keymeta.get [cname 1] key1] "meta_with_expire"
470
471 flushallAndVerifyCleanup
472 } {} {external:skip needs:debug}
473
474 test {RDB: Create keys with upto 7 meta classes, with or without expiry} {
475 # Test all combinations of 1-7 metadata classes, with or without expiry
476 for {set n 1} {$n <= 7} {incr n} {
477 foreach hasExpiry {0 1} {
478 set keyname "key_${n}_exp${hasExpiry}"
479 r set $keyname "value$n"
480
481 # Set expiry if hasExpiry is 1
482 if {$hasExpiry} {
483 set ttl [expr {3600 + $n}]
484 r expire $keyname $ttl
485 # Get the actual expiretime set by Redis to use as expected value
486 set expExpiry [r expiretime $keyname]
487 }
488
489 # Create list of class IDs to attach (1 through n)
490 set class_ids {}
491 for {set i 1} {$i <= $n} {incr i} {
492 lappend class_ids $i
493 }
494
495 # Randomize the order of metadata attachment
496 set class_ids [lshuffle $class_ids]
497
498 # Attach metadata in randomized order
499 foreach cid $class_ids {
500 r keymeta.set [cname $cid] $keyname "meta$cid"
501 }
502
503 # Verify metadata before RDB save
504 # Verify exactly n metadata classes are attached
505 for {set i 1} {$i <= 7} {incr i} {
506 if {$i <= $n} {
507 assert_equal [r keymeta.get [cname $i] $keyname] "meta$i"
508 } else {
509 assert_equal [r keymeta.get [cname $i] $keyname] ""
510 }
511 }
512
513 # Verify expiry before RDB save
514 if {$hasExpiry} {
515 set actual_expiretime [r expiretime $keyname]
516 assert_equal $actual_expiretime $expExpiry
517 }
518
519 # Save and reload from RDB (debug reload preserves modules)
520 r save
521 r debug reload
522
523 # Verify metadata after RDB reload
524 # Verify exactly n metadata classes are still attached
525 for {set i 1} {$i <= 7} {incr i} {
526 if {$i <= $n} {
527 assert_equal [r keymeta.get [cname $i] $keyname] "meta$i"
528 } else {
529 assert_equal [r keymeta.get [cname $i] $keyname] ""
530 }
531 }
532
533 # Verify expiry after RDB reload
534 if {$hasExpiry} {
535 set actual_expiretime [r expiretime $keyname]
536 assert_equal $actual_expiretime $expExpiry
537 } else {
538 # Verify no expiry set
539 assert_equal [r expiretime $keyname] -1
540 }
541 flushallAndVerifyCleanup
542 }
543 }
544 } {} {external:skip needs:save}
545
546 # ========================================================================
547 # RDB Flag Tests: ALLOW_IGNORE, RDBLOAD, RDBSAVE
548 # ========================================================================
549
550 # Test all combinations except the error case (ALLOW_IGNORE=0, RDBLOAD=0, RDBSAVE=1)
551 foreach RDBLOAD {0 1} {
552 foreach RDBSAVE {0 1} {
553 foreach ALLOW_IGNORE {0 1} {
554 # Skip the error case - we'll test it last since it causes RDB load to fail
555 if {!$RDBLOAD && $RDBSAVE && !$ALLOW_IGNORE} { continue }
556
557 test "RDB: SAVE and LOAD (ALLOW_IGNORE=$ALLOW_IGNORE, RDBLOAD=$RDBLOAD, RDBSAVE=$RDBSAVE)" {
558 # Flush all data and save empty RDB to start with a clean slate
559 r flushall
560 r save
561
562 # re-register class 1 with new flags. Expected re-registered same class ID
563 r keymeta.unregister [cname 1]
564 # dummy default spec
565 set newSpec "KEEPONCOPY"
566 if {$ALLOW_IGNORE} { append newSpec ":ALLOWIGNORE" }
567 if {$RDBLOAD} { append newSpec ":RDBLOAD" }
568 if {$RDBSAVE} { append newSpec ":RDBSAVE" }
569
570 # Must reuse same class-id that it had before
571 assert_equal $classes(1) [r keymeta.register [cname 1] 1 $newSpec]
572
573 r set key1 "value1"
574 r keymeta.set [cname 1] key1 "key1_meta1"
575 assert_equal [r keymeta.get [cname 1] key1] "key1_meta1"
576
577 r save
578 r debug reload
579
580 # Metadata is preserved only when BOTH rdb_save AND rdb_load are enabled
581 # Otherwise metadata is lost (either not saved, or saved but not loaded)
582 set metaPreserved [expr {$RDBSAVE && $RDBLOAD}]
583 set expectedMeta [expr {$metaPreserved ? "key1_meta1" : ""}]
584
585 assert_equal [r keymeta.get [cname 1] key1] $expectedMeta
586
587 flushallAndVerifyCleanup
588 } {} {external:skip needs:save}
589 }
590 }
591 }
592
593 # Test the error case last (ALLOW_IGNORE=0, RDBLOAD=0, RDBSAVE=1)
594 # This test causes RDB load to fail, so we test it last to avoid polluting subsequent tests
595 test "RDB: SAVE and LOAD Invalid combination: (ALLOW_IGNORE=0, RDBLOAD=0, RDBSAVE=1)" {
596 # re-register class 1 with RDBSAVE flag but no RDBLOAD or ALLOW_IGNORE
597 r keymeta.unregister [cname 1]
598 set newSpec "KEEPONCOPY:RDBSAVE"
599 assert_equal $classes(1) [r keymeta.register [cname 1] 1 $newSpec]
600
601 r set key1 "value1"
602 r keymeta.set [cname 1] key1 "key1_meta1"
603 assert_equal [r keymeta.get [cname 1] key1] "key1_meta1"
604
605 r save
606
607 # This combination causes RDB load to fail because:
608 # - Metadata was saved (RDBSAVE=1)
609 # - Class has no rdb_load callback (RDBLOAD=0)
610 # - Errors are not ignored (ALLOW_IGNORE=0)
611 catch {r debug reload} err
612 assert_match "*Error trying to load the RDB dump*" $err
613 } {} {external:skip needs:save}
614
615 # ========================================================================
616 # DUMP/RESTORE Tests
617 # ========================================================================
618
619 test {DUMP/RESTORE: 1 to 7 metadata classes, optional TTL} {
620 foreach withTTL {0 1} {
621 for {set numClasses 1} {$numClasses < 8} {incr numClasses} {
622 # Re-register classes with RDBLOAD and RDBSAVE flags
623 for {set cid 1} {$cid <= $numClasses} {incr cid} {
624 r keymeta.unregister [cname $cid]
625 assert_equal $classes($cid) [r keymeta.register [cname $cid] 1 $classesSpec($cid)]
626 }
627
628 # Create key with metadata classes
629 r set key1 "value1"
630 for {set i 1} {$i <= $numClasses} {incr i} {
631 r keymeta.set [cname $i] key1 "meta${i}_value"
632 }
633
634 if {$withTTL} { r expire key1 10000 }
635
636 # Verify all metadata before DUMP
637 for {set i 1} {$i <= $numClasses} {incr i} {
638 assert_equal [r keymeta.get [cname $i] key1] "meta${i}_value"
639 }
640
641 # DUMP the key
642 set encoded [r dump key1]
643
644 # Delete and RESTORE
645 r del key1
646 r restore key1 [expr {$withTTL ? 10000 : 0}] $encoded
647
648 # Verify all metadata was restored
649 assert_equal [r get key1] "value1"
650 for {set i 1} {$i <= $numClasses} {incr i} {
651 assert_equal [r keymeta.get [cname $i] key1] "meta${i}_value"
652 }
653 if {$withTTL} { assert_range [r pttl key1] 9000 10000 }
654
655 flushallAndVerifyCleanup
656 }
657 }
658 }
659
660 test {DUMP/RESTORE: REPLACE with metadata} {
661 # Create key with metadata
662 r set key1 value1
663 r keymeta.set [cname 1] key1 "meta1_original"
664
665 # DUMP the key
666 set encoded1 [r dump key1]
667
668 # Create different key with different metadata
669 r set key1 value2
670 r keymeta.set [cname 1] key1 "meta1_new"
671
672 # DUMP the second version
673 set encoded2 [r dump key1]
674
675 # Delete and restore first version
676 r del key1
677 r restore key1 0 $encoded1
678 assert_equal [r get key1] "value1"
679 assert_equal [r keymeta.get [cname 1] key1] "meta1_original"
680
681 # RESTORE second version with REPLACE
682 r restore key1 0 $encoded2 replace
683 assert_equal [r get key1] "value2"
684 assert_equal [r keymeta.get [cname 1] key1] "meta1_new"
685
686 flushallAndVerifyCleanup
687 }
688
689
690 # Test all combinations except the error case (ALLOW_IGNORE=0, RDBLOAD=0, RDBSAVE=1)
691 foreach RDBLOAD {0 1} {
692 foreach RDBSAVE {0 1} {
693 foreach ALLOW_IGNORE {0 1} {
694 # Skip the error case - we'll test it last since it causes RESTORE to fail
695 if {!$RDBLOAD && $RDBSAVE && !$ALLOW_IGNORE} { continue }
696
697 test "DUMP/RESTORE: (ALLOW_IGNORE=$ALLOW_IGNORE, RDBLOAD=$RDBLOAD, RDBSAVE=$RDBSAVE)" {
698 # re-register class 1 with new flags. Expected re-registered same class ID
699 r keymeta.unregister [cname 1]
700 # dummy default spec
701 set newSpec "KEEPONCOPY"
702 if {$ALLOW_IGNORE} { append newSpec ":ALLOWIGNORE" }
703 if {$RDBLOAD} { append newSpec ":RDBLOAD" }
704 if {$RDBSAVE} { append newSpec ":RDBSAVE" }
705
706 # Must reuse same class-id that it had before
707 assert_equal $classes(1) [r keymeta.register [cname 1] 1 $newSpec]
708
709 r set key1 "value1"
710 r keymeta.set [cname 1] key1 "key1_meta1"
711 assert_equal [r keymeta.get [cname 1] key1] "key1_meta1"
712
713 # DUMP & RESTORE
714 set encoded [r dump key1]
715 r del key1
716 r restore key1 0 $encoded
717
718 # Metadata is preserved only when BOTH rdb_save AND rdb_load are enabled
719 # Otherwise metadata is lost (either not saved, or saved but not loaded)
720 set metaPreserved [expr {$RDBSAVE && $RDBLOAD}]
721 set expectedMeta [expr {$metaPreserved ? "key1_meta1" : ""}]
722
723 assert_equal [r keymeta.get [cname 1] key1] $expectedMeta
724
725 flushallAndVerifyCleanup
726 }
727 }
728 }
729 }
730
731 # Test the error case last (ALLOW_IGNORE=0, RDBLOAD=0, RDBSAVE=1)
732 # This test causes RESTORE to fail, so we test it last to avoid polluting subsequent tests
733 test "DUMP/RESTORE: Invalid combination: (ALLOW_IGNORE=0, RDBLOAD=0, RDBSAVE=1)" {
734 # re-register class 1 with RDBSAVE flag but no RDBLOAD or ALLOW_IGNORE
735 r keymeta.unregister [cname 1]
736 set newSpec "KEEPONCOPY:RDBSAVE"
737 assert_equal $classes(1) [r keymeta.register [cname 1] 1 $newSpec]
738
739 r set key1 "value1"
740 r keymeta.set [cname 1] key1 "key1_meta1"
741 assert_equal [r keymeta.get [cname 1] key1] "key1_meta1"
742
743 # DUMP the key
744 set encoded [r dump key1]
745
746 # Delete and try to RESTORE
747 r del key1
748
749 # This combination causes RESTORE to fail because:
750 # - Metadata was saved (RDBSAVE=1)
751 # - Class has no rdb_load callback (RDBLOAD=0)
752 # - Errors are not ignored (ALLOW_IGNORE=0)
753 catch {r restore key1 0 $encoded} err
754 assert_match "*Bad data format*" $err
755
756 flushallAndVerifyCleanup
757 }
758}
759
760test "RDB: Load with different module registration order preserves metadata correctly" {
761 # This test verifies out-of-order metadata attachment during RDB load.
762 # When modules register in different order at load time vs save time,
763 # metadata values should still be correctly associated with their classes.
764 start_server {tags {"modules" "external:skip" "cluster:skip"} overrides {enable-debug-command yes}} {
765 r module load $testmodule
766
767 # Helper function to generate class names (needed in inner scope)
768 proc cname {id} { return "CLS$id" }
769
770 # Register classes in order: 1, 2, 3
771 set spec1 "KEEPONCOPY:ALLOWIGNORE:RDBLOAD:RDBSAVE"
772 set spec2 "KEEPONRENAME:ALLOWIGNORE:RDBLOAD:RDBSAVE"
773 set spec3 "KEEPONMOVE:ALLOWIGNORE:RDBLOAD:RDBSAVE"
774
775 set class1 [r keymeta.register [cname 1] 1 $spec1]
776 set class2 [r keymeta.register [cname 2] 1 $spec2]
777 set class3 [r keymeta.register [cname 3] 1 $spec3]
778
779 # Verify class IDs match registration order
780 assert_equal $class1 1 "Class 1 registered first, gets ID 1"
781 assert_equal $class2 2 "Class 2 registered second, gets ID 2"
782 assert_equal $class3 3 "Class 3 registered third, gets ID 3"
783
784 # OUTER SERVER: Create RDB with classes registered in order 1,2,3
785 r flushall
786 r set mykey "myvalue"
787 r keymeta.set [cname 1] mykey "metadata_for_class1"
788 r keymeta.set [cname 2] mykey "metadata_for_class2"
789 r keymeta.set [cname 3] mykey "metadata_for_class3"
790
791 # Verify metadata before save
792 assert_equal [r keymeta.get [cname 1] mykey] "metadata_for_class1"
793 assert_equal [r keymeta.get [cname 2] mykey] "metadata_for_class2"
794 assert_equal [r keymeta.get [cname 3] mykey] "metadata_for_class3"
795
796 r save
797
798 # Get RDB file path & Copy RDB to a temp location with unique name
799 set rdb_dir [lindex [r config get dir] 1]
800 set rdb_file [lindex [r config get dbfilename] 1]
801 set rdb_path [file join $rdb_dir $rdb_file]
802 set temp_rdb [file join $rdb_dir "temp_metadata_outoforder_[pid].rdb"]
803 file copy -force $rdb_path $temp_rdb
804
805 # INNER SERVER: Start new server, register classes in DIFFERENT order, then load RDB
806 start_server [list overrides [list dir $rdb_dir enable-debug-command yes]] {
807 r module load $testmodule
808
809 # Helper function to generate class names (needed in inner scope)
810 proc cname {id} { return "CLS$id" }
811
812 # Register classes in DIFFERENT order: 3, 1, 2
813 # This simulates a server where modules load in different order
814 set spec1 "KEEPONCOPY:ALLOWIGNORE:RDBLOAD:RDBSAVE"
815 set spec2 "KEEPONRENAME:ALLOWIGNORE:RDBLOAD:RDBSAVE"
816 set spec3 "KEEPONMOVE:ALLOWIGNORE:RDBLOAD:RDBSAVE"
817
818 set class3 [r keymeta.register [cname 3] 1 $spec3]
819 set class1 [r keymeta.register [cname 1] 1 $spec1]
820 set class2 [r keymeta.register [cname 2] 1 $spec2]
821
822 # Verify class IDs are assigned by REGISTRATION ORDER, not name
823 # We registered in order 3,1,2, so the runtime IDs are:
824 # - class3 (name "CLS3") gets ID 1 (first registered)
825 # - class1 (name "CLS1") gets ID 2 (second registered)
826 # - class2 (name "CLS2") gets ID 3 (third registered)
827 # This is DIFFERENT from outer server which registered in order 1,2,3
828 assert_equal $class3 1 "Class 3 registered first, gets ID 1"
829 assert_equal $class1 2 "Class 1 registered second, gets ID 2"
830 assert_equal $class2 3 "Class 2 registered third, gets ID 3"
831
832 # Copy the saved RDB to this server's dbfilename
833 set inner_rdb_file [lindex [r config get dbfilename] 1]
834 set inner_rdb_path [file join $rdb_dir $inner_rdb_file]
835 file copy -force $temp_rdb $inner_rdb_path
836
837 # NOW load the RDB (AFTER registration in different order)
838 # Use 'nosave' to reload from the copied RDB without saving current state first
839 r debug reload nosave
840
841 # Verify the key exists
842 assert_equal [r exists mykey] 1 "Key should exist after RDB load"
843 assert_equal [r get mykey] "myvalue" "Key value should be preserved"
844
845 # Verify metadata values are correctly associated with their classes
846 # WITHOUT metadata would be swapped because:
847 # - At SAVE time (outer): classes registered in order 1,2,3
848 # - At LOAD time (inner): classes registered in order 3,1,2
849 # - RDB contains metadata in saved order, but keyMetaClassLookupByName
850 # maps them back to correct classes by NAME, not by registration order
851 assert_equal [r keymeta.get [cname 1] mykey] "metadata_for_class1"
852 assert_equal [r keymeta.get [cname 2] mykey] "metadata_for_class2"
853 assert_equal [r keymeta.get [cname 3] mykey] "metadata_for_class3"
854 }
855
856 # Cleanup temp file
857 file delete $temp_rdb
858
859 }
860} {} {external:skip needs:save}
861
862test "RDB: File size same with/without metadata when no rdb_save callback" {
863 # This test verifies that when a metadata class has no rdb_save callback,
864 # the metadata is not serialized to RDB, so the RDB file size should be
865 # approximately the same (within a small tolerance for header differences).
866
867 start_server {tags {"modules" "external:skip" "cluster:skip"} overrides {enable-debug-command yes}} {
868 r module load $testmodule
869
870 # Get RDB directory
871 set rdb_dir [lindex [r config get dir] 1]
872 set rdb_file [lindex [r config get dbfilename] 1]
873 set rdb_path [file join $rdb_dir $rdb_file]
874
875 # Test 1: Create key WITHOUT metadata and save
876 r flushall
877 r set key1 "test_value_12345"
878 r save
879 set size_without_meta [file size $rdb_path]
880
881 # Test 2: Create identical key WITH metadata (but no rdb_save) and save
882 # Register a class WITHOUT rdb_save callback (RDBSAVE=0)
883 # Use ALLOWIGNORE so loading doesn't fail when metadata is missing
884 set spec "ALLOWIGNORE"
885 r keymeta.register [cname 1] 1 $spec
886
887 r flushall
888 r set key1 "test_value_12345"
889 r keymeta.set [cname 1] key1 "some_metadata_value"
890
891 # Verify metadata is attached
892 assert_equal [r keymeta.get [cname 1] key1] "some_metadata_value"
893
894 r save
895 set size_with_meta [file size $rdb_path]
896
897 # The file sizes should be the same (metadata not serialized)
898 assert_equal $size_without_meta $size_with_meta
899 }
900} {} {external:skip needs:save}
901
902test "Creating key metadata not during OnLoad should fail" {
903 # This time start_server without "enable-debug-command yes"
904 start_server {tags {"modules" "external:skip" "cluster:skip"} overrides {enable-debug-command no}} {
905 r module load $testmodule
906 # Creating a class not during OnLoad should fail
907 catch {r keymeta.register [cname 1] 1 "ALLOWIGNORE"} err
908 assert_match {*failed to create metadata class*} $err
909 }
910} {} {external:skip needs:save}