Skip to content

Commit 378c31c

Browse files
committed
Improve error handling in list operations
1 parent 5e004ea commit 378c31c

File tree

4 files changed

+75
-26
lines changed

4 files changed

+75
-26
lines changed

adapter/dynamodb_transcoder.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,8 @@ func (t *dynamodbTranscoder) valueAttrToOps(key []byte, val attributeValue) (*kv
8787
if len(val.L) > 0 {
8888
var elems []*kv.Elem[kv.OP]
8989
for i, item := range val.L {
90-
if item.S == "" {
91-
return nil, errors.New("only string list items are supported")
90+
if len(item.L) > 0 {
91+
return nil, errors.New("nested lists are not supported")
9292
}
9393
elems = append(elems, &kv.Elem[kv.OP]{
9494
Op: kv.Put,
@@ -110,8 +110,8 @@ func (t *dynamodbTranscoder) valueAttrToOps(key []byte, val attributeValue) (*kv
110110
return &kv.OperationGroup[kv.OP]{IsTxn: true, Elems: elems}, nil
111111
}
112112

113-
// Default: simple string
114-
if val.S == "" {
113+
// Default: simple string (allow empty string). Reject only when both S and L are absent.
114+
if val.S == "" && len(val.L) == 0 {
115115
return nil, errors.New("unsupported attribute type (only S or L of S)")
116116
}
117117
return &kv.OperationGroup[kv.OP]{

adapter/redis.go

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -183,11 +183,8 @@ func (r *RedisServer) set(conn redcon.Conn, cmd redcon.Command) {
183183
return
184184
}
185185
if isList {
186-
// delete list metadata so this becomes a plain string key
187-
if err := r.deleteList(context.Background(), cmd.Args[1]); err != nil {
188-
conn.WriteError(err.Error())
189-
return
190-
}
186+
conn.WriteError("WRONGTYPE Operation against a key holding the wrong kind of value")
187+
return
191188
}
192189

193190
res, err := r.redisTranscoder.SetToRequest(cmd.Args[1], cmd.Args[2])
@@ -629,15 +626,15 @@ func (t *txnContext) applyLRange(cmd redcon.Command) (redisResult, error) {
629626
}
630627

631628
func parseRangeBounds(startRaw, endRaw []byte, total int) (int, int, error) {
632-
start, err := strconv.Atoi(string(startRaw))
629+
start, err := parseInt(startRaw)
633630
if err != nil {
634-
return 0, 0, errors.WithStack(err)
631+
return 0, 0, err
635632
}
636-
end, err := strconv.Atoi(string(endRaw))
633+
end, err := parseInt(endRaw)
637634
if err != nil {
638-
return 0, 0, errors.WithStack(err)
635+
return 0, 0, err
639636
}
640-
s, e := clampRange(start, end, total)
637+
s, e := clampRange(int(start), int(end), total)
641638
return s, e, nil
642639
}
643640

@@ -723,6 +720,10 @@ func (t *txnContext) buildListElems() ([]*kv.Elem[kv.OP], error) {
723720
userKey := []byte(k)
724721

725722
if st.deleted {
723+
// delete all persisted list items
724+
for seq := st.meta.Head; seq < st.meta.Tail; seq++ {
725+
elems = append(elems, &kv.Elem[kv.OP]{Op: kv.Del, Key: listItemKey(userKey, seq)})
726+
}
726727
elems = append(elems, &kv.Elem[kv.OP]{Op: kv.Del, Key: listMetaKey(userKey)})
727728
continue
728729
}
@@ -884,17 +885,32 @@ func (r *RedisServer) listRPush(ctx context.Context, key []byte, values [][]byte
884885
}
885886

886887
func (r *RedisServer) deleteList(ctx context.Context, key []byte) error {
887-
_, exists, err := r.loadListMeta(ctx, key)
888+
meta, exists, err := r.loadListMeta(ctx, key)
888889
if err != nil {
889890
return err
890891
}
891892
if !exists {
892893
return nil
893894
}
894895

895-
ops := []*kv.Elem[kv.OP]{
896-
{Op: kv.Del, Key: listMetaKey(key)},
896+
start := listItemKey(key, math.MinInt64)
897+
end := listItemKey(key, math.MaxInt64)
898+
899+
kvs, err := r.store.Scan(ctx, start, end, math.MaxInt)
900+
if err != nil {
901+
return errors.WithStack(err)
902+
}
903+
904+
ops := make([]*kv.Elem[kv.OP], 0, len(kvs)+1)
905+
for _, kvp := range kvs {
906+
ops = append(ops, &kv.Elem[kv.OP]{Op: kv.Del, Key: kvp.Key})
897907
}
908+
// delete meta last
909+
ops = append(ops, &kv.Elem[kv.OP]{Op: kv.Del, Key: listMetaKey(key)})
910+
911+
// ensure meta bounds consistent even if scan missed (in case of empty list)
912+
_ = meta
913+
898914
group := &kv.OperationGroup[kv.OP]{IsTxn: true, Elems: ops}
899915
_, err = r.coordinator.Dispatch(group)
900916
return errors.WithStack(err)
@@ -966,7 +982,16 @@ func (r *RedisServer) proxyLRange(key []byte, startRaw, endRaw []byte) ([]string
966982
cli := redis.NewClient(&redis.Options{Addr: leaderAddr})
967983
defer func() { _ = cli.Close() }()
968984

969-
res, err := cli.LRange(context.Background(), string(key), parseInt(startRaw), parseInt(endRaw)).Result()
985+
start, err := parseInt(startRaw)
986+
if err != nil {
987+
return nil, err
988+
}
989+
end, err := parseInt(endRaw)
990+
if err != nil {
991+
return nil, err
992+
}
993+
994+
res, err := cli.LRange(context.Background(), string(key), start, end).Result()
970995
return res, errors.WithStack(err)
971996
}
972997

@@ -992,9 +1017,9 @@ func (r *RedisServer) proxyRPush(key []byte, values [][]byte) (int64, error) {
9921017
return res, errors.WithStack(err)
9931018
}
9941019

995-
func parseInt(b []byte) int64 {
996-
i, _ := strconv.ParseInt(string(b), 10, 64)
997-
return i
1020+
func parseInt(b []byte) (int64, error) {
1021+
i, err := strconv.ParseInt(string(b), 10, 64)
1022+
return i, errors.WithStack(err)
9981023
}
9991024

10001025
// tryLeaderGet proxies a GET to the current Raft leader, returning the value and

adapter/test_util.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ import (
1010
"testing"
1111
"time"
1212

13-
"golang.org/x/sys/unix"
14-
1513
"github.com/Jille/raft-grpc-leader-rpc/leaderhealth"
1614
transport "github.com/Jille/raft-grpc-transport"
1715
"github.com/Jille/raftadmin"
@@ -23,6 +21,7 @@ import (
2321
"github.com/hashicorp/raft"
2422
"github.com/stretchr/testify/assert"
2523
"github.com/stretchr/testify/require"
24+
"golang.org/x/sys/unix"
2625
"google.golang.org/grpc"
2726
"google.golang.org/grpc/credentials/insecure"
2827
)

store/list_store.go

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ package store
33
import (
44
"bytes"
55
"context"
6+
"encoding/hex"
67
"encoding/json"
7-
"fmt"
88
"math"
99

1010
"github.com/cockroachdb/errors"
@@ -220,7 +220,32 @@ func ListMetaKey(userKey []byte) []byte {
220220

221221
// ListItemKey builds the item key for a user key and sequence number.
222222
func ListItemKey(userKey []byte, seq int64) []byte {
223-
return []byte(fmt.Sprintf("%s%s|%0*d", ListItemPrefix, userKey, ListSeqWidth, seq))
223+
// Offset sign bit (seq ^ minInt64) to preserve order, then big-endian encode and hex.
224+
var raw [8]byte
225+
encodeSortableInt64(raw[:], seq)
226+
hexSeq := make([]byte, hex.EncodedLen(len(raw)))
227+
hex.Encode(hexSeq, raw[:])
228+
229+
buf := make([]byte, 0, len(ListItemPrefix)+len(userKey)+1+len(hexSeq))
230+
buf = append(buf, ListItemPrefix...)
231+
buf = append(buf, userKey...)
232+
buf = append(buf, '|')
233+
buf = append(buf, hexSeq...)
234+
return buf
235+
}
236+
237+
// encodeSortableInt64 writes seq with sign bit flipped (seq ^ minInt64) in big-endian order.
238+
const sortableInt64Bytes = 8
239+
240+
func encodeSortableInt64(dst []byte, seq int64) {
241+
if len(dst) < sortableInt64Bytes {
242+
return
243+
}
244+
sortable := seq ^ math.MinInt64
245+
for i := sortableInt64Bytes - 1; i >= 0; i-- {
246+
dst[i] = byte(sortable)
247+
sortable >>= 8
248+
}
224249
}
225250

226251
func clampRange(start, end, length int) (int, int) {
@@ -263,7 +288,7 @@ func ExtractListUserKey(key []byte) []byte {
263288
trimmed := bytes.TrimPrefix(key, []byte(ListItemPrefix))
264289
idx := bytes.LastIndexByte(trimmed, '|')
265290
if idx == -1 {
266-
return trimmed
291+
return nil
267292
}
268293
return trimmed[:idx]
269294
default:

0 commit comments

Comments
 (0)