@@ -139,6 +139,116 @@ start_server {tags "expire"} {
139139 }
140140}
141141
142+ # Guard tests for scanMetaExpireIfNeeded ordering regression (fixed in dfa99d68a).
143+ # Bug: scanMetaExpireIfNeeded was placed AFTER stringmatchlen / type filters,
144+ # so expired cold keys that didn't match MATCH or TYPE were never deleted.
145+ # These tests must FAIL on pre-fix code and PASS on post-fix code.
146+ start_server {tags " scanMetaExpireIfNeeded" } {
147+ r config set swap-debug-evict-keys 0
148+
149+ test {scanMetaExpireIfNeeded: expired cold keys deleted even when MATCH does not match} {
150+ # Pre-fix: stringmatchlen ran first; non-matching keys skipped before
151+ # scanMetaExpireIfNeeded, so they were never submitted for deletion.
152+ r debug set-active-expire 0
153+ r flushall
154+
155+ foreach key {expire:1 expire:2 expire:3} {
156+ r psetex $key 500 bar
157+ r swap.evict $key
158+ wait_key_cold r $key
159+ }
160+ after 600
161+
162+ # Full scan with a pattern that matches NONE of the expired keys.
163+ set cursor 0
164+ while 1 {
165+ set res [r scan $cursor match other:*]
166+ set cursor [lindex $res 0]
167+ if {$cursor == 0} break
168+ }
169+
170+ # scanMetaExpireIfNeeded must fire regardless of MATCH.
171+ wait_for_condition 100 100 {
172+ [r dbsize] eq 0
173+ } else {
174+ fail " Expired cold keys not deleted when MATCH pattern did not match"
175+ }
176+ foreach key {expire:1 expire:2 expire:3} {
177+ assert_equal [rio_get_meta r $key ] {}
178+ }
179+
180+ r debug set-active-expire 1
181+ }
182+
183+ test {scanMetaExpireIfNeeded: expired cold keys deleted even when TYPE filter does not match} {
184+ # Pre-fix: type filter ran first; non-matching keys skipped before
185+ # scanMetaExpireIfNeeded, so they were never submitted for deletion.
186+ r debug set-active-expire 0
187+ r flushall
188+
189+ # String keys will not match TYPE hash.
190+ foreach key {strexp:1 strexp:2 strexp:3} {
191+ r psetex $key 500 bar
192+ r swap.evict $key
193+ wait_key_cold r $key
194+ }
195+ after 600
196+
197+ set cursor 0
198+ while 1 {
199+ set res [r scan $cursor type hash]
200+ set cursor [lindex $res 0]
201+ if {$cursor == 0} break
202+ }
203+
204+ # scanMetaExpireIfNeeded must fire regardless of TYPE filter.
205+ wait_for_condition 100 100 {
206+ [r dbsize] eq 0
207+ } else {
208+ fail " Expired cold keys not deleted when TYPE filter did not match"
209+ }
210+ foreach key {strexp:1 strexp:2 strexp:3} {
211+ assert_equal [rio_get_meta r $key ] {}
212+ }
213+
214+ r debug set-active-expire 1
215+ }
216+
217+ test {scanMetaExpireIfNeeded: expired cold keys not matching MATCH are also deleted} {
218+ # Same regression with a mixed key set: some keys match the pattern,
219+ # some do not. Pre-fix: non-matching key (other:99) is never deleted.
220+ r debug set-active-expire 0
221+ r flushall
222+
223+ foreach key {match:1 match:2 other:99} {
224+ r psetex $key 500 bar
225+ r swap.evict $key
226+ wait_key_cold r $key
227+ }
228+ after 600
229+
230+ set cursor 0
231+ while 1 {
232+ set res [r scan $cursor match match:*]
233+ set cursor [lindex $res 0]
234+ assert_equal [llength [lindex $res 1]] 0
235+ if {$cursor == 0} break
236+ }
237+
238+ # All three keys must be deleted, including other:99 which didn't match.
239+ wait_for_condition 100 100 {
240+ [r dbsize] eq 0
241+ } else {
242+ fail " Expired cold key (other:99) not deleted despite MATCH mismatch"
243+ }
244+ foreach key {match:1 match:2 other:99} {
245+ assert_equal [rio_get_meta r $key ] {}
246+ }
247+
248+ r debug set-active-expire 1
249+ }
250+ }
251+
142252start_server {tags " unlink cold string" } {
143253 test {swap out and unlink cold string} {
144254 r set k v
0 commit comments