Skip to content

Commit dac10aa

Browse files
authored
feat: CacheMarshal and CacheUnmarshalView for third party cache libraries (#693)
Signed-off-by: Rueian <[email protected]>
1 parent d96c8ce commit dac10aa

File tree

2 files changed

+146
-0
lines changed

2 files changed

+146
-0
lines changed

message.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package rueidis
22

33
import (
4+
"bytes"
5+
"encoding/binary"
46
"encoding/json"
57
"errors"
68
"fmt"
@@ -529,6 +531,104 @@ type RedisMessage struct {
529531
ttl [7]byte
530532
}
531533

534+
func (m *RedisMessage) cachesize() int {
535+
n := 9 // typ (1) + length (8) TODO: can we use VarInt instead of fixed 8 bytes for length?
536+
switch m.typ {
537+
case typeInteger, typeNull, typeBool:
538+
case typeArray, typeMap, typeSet:
539+
for _, val := range m.values {
540+
n += val.cachesize()
541+
}
542+
default:
543+
n += len(m.string)
544+
}
545+
return n
546+
}
547+
548+
func (m *RedisMessage) serialize(o *bytes.Buffer) {
549+
var buf [8]byte // TODO: can we use VarInt instead of fixed 8 bytes for length?
550+
o.WriteByte(m.typ)
551+
switch m.typ {
552+
case typeInteger, typeNull, typeBool:
553+
binary.BigEndian.PutUint64(buf[:], uint64(m.integer))
554+
o.Write(buf[:])
555+
case typeArray, typeMap, typeSet:
556+
binary.BigEndian.PutUint64(buf[:], uint64(len(m.values)))
557+
o.Write(buf[:])
558+
for _, val := range m.values {
559+
val.serialize(o)
560+
}
561+
default:
562+
binary.BigEndian.PutUint64(buf[:], uint64(len(m.string)))
563+
o.Write(buf[:])
564+
o.WriteString(m.string)
565+
}
566+
}
567+
568+
var ErrCacheUnmarshal = errors.New("cache unmarshal error")
569+
570+
func (m *RedisMessage) unmarshalView(c int64, buf []byte) (int64, error) {
571+
var err error
572+
if int64(len(buf)) < c+9 {
573+
return 0, ErrCacheUnmarshal
574+
}
575+
m.typ = buf[c]
576+
c += 1
577+
m.integer = int64(binary.BigEndian.Uint64(buf[c : c+8]))
578+
c += 8 // TODO: can we use VarInt instead of fixed 8 bytes for length?
579+
switch m.typ {
580+
case typeInteger, typeNull, typeBool:
581+
case typeArray, typeMap, typeSet:
582+
m.values = make([]RedisMessage, m.integer)
583+
m.integer = 0
584+
for i := range m.values {
585+
if c, err = m.values[i].unmarshalView(c, buf); err != nil {
586+
break
587+
}
588+
}
589+
default:
590+
if int64(len(buf)) < c+m.integer {
591+
return 0, ErrCacheUnmarshal
592+
}
593+
m.string = BinaryString(buf[c : c+m.integer])
594+
c += m.integer
595+
m.integer = 0
596+
}
597+
return c, err
598+
}
599+
600+
// CacheSize returns the buffer size needed by the CacheMarshal.
601+
func (m *RedisMessage) CacheSize() int {
602+
return m.cachesize() + 7 // 7 for ttl
603+
}
604+
605+
// CacheMarshal writes serialized RedisMessage to the provided buffer.
606+
// If the provided buffer is nil, CacheMarshal will allocate one.
607+
// Note that output format is not compatible with different client versions.
608+
func (m *RedisMessage) CacheMarshal(buf []byte) []byte {
609+
if buf == nil {
610+
buf = make([]byte, 0, m.CacheSize())
611+
}
612+
o := bytes.NewBuffer(buf)
613+
o.Write(m.ttl[:7])
614+
m.serialize(o)
615+
return o.Bytes()
616+
}
617+
618+
// CacheUnmarshalView construct the RedisMessage from the buffer produced by CacheMarshal.
619+
// Note that the buffer can't be reused after CacheUnmarshalView since it uses unsafe.String on top of the buffer.
620+
func (m *RedisMessage) CacheUnmarshalView(buf []byte) error {
621+
if len(buf) < 7 {
622+
return ErrCacheUnmarshal
623+
}
624+
copy(m.ttl[:7], buf[:7])
625+
if _, err := m.unmarshalView(7, buf); err != nil {
626+
return err
627+
}
628+
m.attrs = cacheMark
629+
return nil
630+
}
631+
532632
// IsNil check if message is a redis nil response
533633
func (m *RedisMessage) IsNil() bool {
534634
return m.typ == typeNull

message_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1996,4 +1996,50 @@ func TestRedisMessage(t *testing.T) {
19961996
}
19971997
}
19981998
})
1999+
2000+
t.Run("CacheMarshal and CacheUnmarshalView", func(t *testing.T) {
2001+
m1 := RedisMessage{typ: '_'}
2002+
m2 := RedisMessage{typ: '+', string: "random"}
2003+
m3 := RedisMessage{typ: '#', integer: 1}
2004+
m4 := RedisMessage{typ: ':', integer: -1234}
2005+
m5 := RedisMessage{typ: ',', string: "-1.5"}
2006+
m6 := RedisMessage{typ: '%', values: nil}
2007+
m7 := RedisMessage{typ: '~', values: []RedisMessage{m1, m2, m3, m4, m5, m6}}
2008+
m8 := RedisMessage{typ: '*', values: []RedisMessage{m1, m2, m3, m4, m5, m6, m7}}
2009+
msgs := []RedisMessage{m1, m2, m3, m4, m5, m6, m7, m8}
2010+
now := time.Now()
2011+
for i := range msgs {
2012+
msgs[i].setExpireAt(now.Add(time.Second * time.Duration(i)).UnixMilli())
2013+
}
2014+
for i, m1 := range msgs {
2015+
siz := m1.CacheSize()
2016+
bs1 := m1.CacheMarshal(nil)
2017+
if len(bs1) != siz {
2018+
t.Fatal("size not match")
2019+
}
2020+
bs2 := m1.CacheMarshal(bs1)
2021+
if !bytes.Equal(bs2[:siz], bs2[siz:]) {
2022+
t.Fatal("byte not match")
2023+
}
2024+
var m2 RedisMessage
2025+
if err := m2.CacheUnmarshalView(bs1); err != nil {
2026+
t.Fatal(err)
2027+
}
2028+
if m1.String() != m2.String() {
2029+
t.Fatal("content not match")
2030+
}
2031+
if !m2.IsCacheHit() {
2032+
t.Fatal("should be cache hit")
2033+
}
2034+
if m2.CachePXAT() != now.Add(time.Second*time.Duration(i)).UnixMilli() {
2035+
t.Fatal("should have the same ttl")
2036+
}
2037+
for l := 0; l < siz; l++ {
2038+
var m3 RedisMessage
2039+
if err := m3.CacheUnmarshalView(bs2[:l]); err != ErrCacheUnmarshal {
2040+
t.Fatal("should fail as expected")
2041+
}
2042+
}
2043+
}
2044+
})
19992045
}

0 commit comments

Comments
 (0)