Skip to content

Commit 961206c

Browse files
Copilotbootjp
andcommitted
adapter: fix DEL+recreate list leaving orphaned storage items
Co-authored-by: bootjp <1306365+bootjp@users.noreply.github.com> Agent-Logs-Url: https://github.com/bootjp/elastickv/sessions/a35c08cd-cb6d-4ea5-9605-cd91074de396
1 parent dfcff5a commit 961206c

File tree

2 files changed

+56
-14
lines changed

2 files changed

+56
-14
lines changed

adapter/redis_lua_compat_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,3 +172,34 @@ return {moved, redis.call("LLEN", KEYS[1])}
172172
require.NoError(t, err)
173173
require.Greater(t, ttl, time.Duration(0))
174174
}
175+
176+
func TestRedis_LuaDelAndRecreateListNoOrphan(t *testing.T) {
177+
nodes, _, _ := createNode(t, 3)
178+
defer shutdown(nodes)
179+
180+
ctx := context.Background()
181+
rdb := redis.NewClient(&redis.Options{Addr: nodes[0].redisAddress})
182+
defer func() { _ = rdb.Close() }()
183+
184+
// Pre-populate a list so there are existing storage items.
185+
require.NoError(t, rdb.RPush(ctx, "mylist", "a", "b", "c").Err())
186+
187+
// In a single Lua script: DEL the key, then recreate it as a list.
188+
// After the script, only the new items should be visible; the old
189+
// [a, b, c] items must not be orphaned/leaked in storage.
190+
result, err := rdb.Eval(ctx, `
191+
redis.call("DEL", KEYS[1])
192+
redis.call("RPUSH", KEYS[1], "d")
193+
return redis.call("LRANGE", KEYS[1], 0, -1)
194+
`, []string{"mylist"}).Result()
195+
require.NoError(t, err)
196+
197+
values, ok := result.([]any)
198+
require.True(t, ok)
199+
require.Equal(t, []any{"d"}, values, "list should contain only the newly pushed item")
200+
201+
// Verify via a plain LRANGE that the old items are gone.
202+
final, err := rdb.LRange(ctx, "mylist", 0, -1).Result()
203+
require.NoError(t, err)
204+
require.Equal(t, []string{"d"}, final)
205+
}

adapter/redis_lua_context.go

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ type luaScriptContext struct {
1919
startTS uint64
2020
readPin *kv.ActiveTimestampToken
2121

22-
touched map[string]struct{}
23-
deleted map[string]bool
22+
touched map[string]struct{}
23+
deleted map[string]bool
24+
everDeleted map[string]bool
2425

2526
strings map[string]*luaStringState
2627
lists map[string]*luaListState
@@ -189,18 +190,19 @@ var luaRenameHandlers = map[redisValueType]luaRenameHandler{
189190
func newLuaScriptContext(server *RedisServer) *luaScriptContext {
190191
startTS := server.readTS()
191192
return &luaScriptContext{
192-
server: server,
193-
startTS: startTS,
194-
readPin: server.pinReadTS(startTS),
195-
touched: map[string]struct{}{},
196-
deleted: map[string]bool{},
197-
strings: map[string]*luaStringState{},
198-
lists: map[string]*luaListState{},
199-
hashes: map[string]*luaHashState{},
200-
sets: map[string]*luaSetState{},
201-
zsets: map[string]*luaZSetState{},
202-
streams: map[string]*luaStreamState{},
203-
ttls: map[string]*luaTTLState{},
193+
server: server,
194+
startTS: startTS,
195+
readPin: server.pinReadTS(startTS),
196+
touched: map[string]struct{}{},
197+
deleted: map[string]bool{},
198+
everDeleted: map[string]bool{},
199+
strings: map[string]*luaStringState{},
200+
lists: map[string]*luaListState{},
201+
hashes: map[string]*luaHashState{},
202+
sets: map[string]*luaSetState{},
203+
zsets: map[string]*luaZSetState{},
204+
streams: map[string]*luaStreamState{},
205+
ttls: map[string]*luaTTLState{},
204206
}
205207
}
206208

@@ -265,6 +267,7 @@ func (c *luaScriptContext) deleteLogical(key []byte) {
265267
k := string(key)
266268
c.markTouched(key)
267269
c.deleted[k] = true
270+
c.everDeleted[k] = true
268271
c.clearTTL(key)
269272

270273
if st, ok := c.strings[k]; ok {
@@ -2517,6 +2520,14 @@ func (c *luaScriptContext) listCommitPlan(key string) (luaCommitPlan, error) {
25172520
elems, err := c.listCommitElems(key)
25182521
return luaCommitPlan{elems: elems}, err
25192522
}
2523+
// If the key was deleted earlier in this script and later recreated as a
2524+
// list, we must perform a full rewrite (preserveExisting=false) so that
2525+
// deleteLogicalKeyElems is called and any orphaned storage items from the
2526+
// previous incarnation of the key are cleaned up before writing the delta.
2527+
if c.everDeleted[key] {
2528+
elems, err := c.listCommitElems(key)
2529+
return luaCommitPlan{elems: elems}, err
2530+
}
25202531
elems, err := c.listDeltaCommitElems(key, st)
25212532
return luaCommitPlan{preserveExisting: true, elems: elems}, err
25222533
}

0 commit comments

Comments
 (0)