Skip to content

Commit 36d5281

Browse files
bborehambradfitz
authored andcommitted
memcache: optimize scanGetResponseLine
1 parent bf2c6a2 commit 36d5281

2 files changed

Lines changed: 112 additions & 9 deletions

File tree

memcache/memcache.go

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"errors"
2525
"fmt"
2626
"io"
27+
"math"
2728
"net"
2829
"strconv"
2930
"strings"
@@ -549,17 +550,52 @@ func parseGetResponse(r *bufio.Reader, conn *conn, cb func(*Item)) error {
549550
// scanGetResponseLine populates it and returns the declared size of the item.
550551
// It does not read the bytes of the item.
551552
func scanGetResponseLine(line []byte, it *Item) (size int, err error) {
552-
pattern := "VALUE %s %d %d %d\r\n"
553-
dest := []interface{}{&it.Key, &it.Flags, &size, &it.CasID}
554-
if bytes.Count(line, space) == 3 {
555-
pattern = "VALUE %s %d %d\r\n"
556-
dest = dest[:3]
557-
}
558-
n, err := fmt.Sscanf(string(line), pattern, dest...)
559-
if err != nil || n != len(dest) {
553+
errf := func(line []byte) (int, error) {
560554
return -1, fmt.Errorf("memcache: unexpected line in get response: %q", line)
561555
}
562-
return size, nil
556+
if !bytes.HasPrefix(line, []byte("VALUE ")) || !bytes.HasSuffix(line, []byte("\r\n")) {
557+
return errf(line)
558+
}
559+
s := string(line[6 : len(line)-2])
560+
var rest string
561+
var found bool
562+
it.Key, rest, found = cut(s, ' ')
563+
if !found {
564+
return errf(line)
565+
}
566+
val, rest, found := cut(rest, ' ')
567+
if !found {
568+
return errf(line)
569+
}
570+
flags64, err := strconv.ParseUint(val, 10, 32)
571+
if err != nil {
572+
return errf(line)
573+
}
574+
it.Flags = uint32(flags64)
575+
val, rest, found = cut(rest, ' ')
576+
size64, err := strconv.ParseUint(val, 10, 32)
577+
if err != nil {
578+
return errf(line)
579+
}
580+
if size64 > math.MaxInt { // Can happen if int is 32-bit
581+
return errf(line)
582+
}
583+
if !found { // final CAS ID is optional.
584+
return int(size64), nil
585+
}
586+
it.CasID, err = strconv.ParseUint(rest, 10, 64)
587+
if err != nil {
588+
return errf(line)
589+
}
590+
return int(size64), nil
591+
}
592+
593+
// Similar to strings.Cut in Go 1.18, but sep can only be 1 byte.
594+
func cut(s string, sep byte) (before, after string, found bool) {
595+
if i := strings.IndexByte(s, sep); i >= 0 {
596+
return s[:i], s[i+1:], true
597+
}
598+
return s, "", false
563599
}
564600

565601
// Set writes the given item, unconditionally.

memcache/memcache_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,3 +435,70 @@ func BenchmarkScanGetResponseLine(b *testing.B) {
435435
}
436436
}
437437
}
438+
439+
func TestScanGetResponseLine(t *testing.T) {
440+
tests := []struct {
441+
name string
442+
line string
443+
wantKey string
444+
wantFlags uint32
445+
wantCasid uint64
446+
wantSize int
447+
wantErr bool
448+
}{
449+
{name: "blank", line: "",
450+
wantErr: true},
451+
{name: "malformed1", line: "VALU foobar1234 1 4096\r\n",
452+
wantErr: true},
453+
{name: "malformed2", line: "VALUEfoobar1234 1 4096\r\n",
454+
wantErr: true},
455+
{name: "malformed3", line: "VALUE foobar1234 14096\r\n",
456+
wantErr: true},
457+
{name: "malformed4", line: "VALUE foobar123414096\r\n",
458+
wantErr: true},
459+
{name: "no-eol", line: "VALUE foobar1234 1 4096",
460+
wantErr: true},
461+
{name: "basic", line: "VALUE foobar1234 1 4096\r\n",
462+
wantKey: "foobar1234", wantFlags: 1, wantSize: 4096},
463+
{name: "casid", line: "VALUE foobar1234 1 4096 1234\r\n",
464+
wantKey: "foobar1234", wantFlags: 1, wantSize: 4096, wantCasid: 1234},
465+
{name: "flags-max-uint32", line: "VALUE key 4294967295 1\r\n",
466+
wantKey: "key", wantFlags: 4294967295, wantSize: 1},
467+
{name: "flags-overflow", line: "VALUE key 4294967296 1\r\n",
468+
wantErr: true},
469+
{name: "size-max-uint32", line: "VALUE key 1 2147483647\r\n",
470+
wantKey: "key", wantFlags: 1, wantSize: 2147483647},
471+
{name: "size-overflow", line: "VALUE key 1 4294967296\r\n",
472+
wantErr: true},
473+
{name: "casid-overflow", line: "VALUE key 1 4096 18446744073709551616\r\n",
474+
wantErr: true},
475+
}
476+
for _, tt := range tests {
477+
t.Run(tt.name, func(t *testing.T) {
478+
var got Item
479+
gotSize, err := scanGetResponseLine([]byte(tt.line), &got)
480+
if tt.wantErr {
481+
if err == nil {
482+
t.Errorf("scanGetResponseLine() should have returned error")
483+
}
484+
return
485+
}
486+
if err != nil {
487+
t.Errorf("scanGetResponseLine() returned error %s", err)
488+
return
489+
}
490+
if got.Key != tt.wantKey {
491+
t.Errorf("key = %v, want %v", got.Key, tt.wantKey)
492+
}
493+
if got.Flags != tt.wantFlags {
494+
t.Errorf("flags = %v, want %v", got.Flags, tt.wantFlags)
495+
}
496+
if got.CasID != tt.wantCasid {
497+
t.Errorf("flags = %v, want %v", got.CasID, tt.wantCasid)
498+
}
499+
if gotSize != tt.wantSize {
500+
t.Errorf("size = %v, want %v", gotSize, tt.wantSize)
501+
}
502+
})
503+
}
504+
}

0 commit comments

Comments
 (0)