Skip to content

Commit

Permalink
feat: add .rdb file parser & KEYS command (#5)
Browse files Browse the repository at this point in the history
* feat: .rdb parser

* feat: support pattern matching for KEYS

* refactor: default dir

* fix: lint err
  • Loading branch information
mhughdo authored Jun 27, 2024
1 parent 6b1a4f5 commit 2538a42
Show file tree
Hide file tree
Showing 22 changed files with 805 additions and 50 deletions.
Binary file added .DS_Store
Binary file not shown.
13 changes: 7 additions & 6 deletions app/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ var (
)

func run(ctx context.Context, _ io.Writer, _ []string) error {
flag.Parse()
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(ctx)
sigCh := make(chan os.Signal, 1)
logger.Info(ctx, "oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo")
flag.Parse()
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
defer signal.Stop(sigCh)
cfg := config.NewConfig()
Expand All @@ -38,8 +39,8 @@ func run(ctx context.Context, _ io.Writer, _ []string) error {
}
server := server.NewServer(cfg)
go func() {
if err := server.Listen(ctx); err != nil {
logger.Error(ctx, "failed to listen, err: %v", err)
if err := server.Start(ctx); err != nil {
logger.Error(ctx, "failed to start the server, err: %v", err)
}
cancel()
}()
Expand All @@ -48,15 +49,15 @@ func run(ctx context.Context, _ io.Writer, _ []string) error {
case <-ctx.Done():
logger.Info(ctx, "context done, shutting down")
case <-sigCh:
logger.Info(ctx, "received signal, shutting down")
logger.Info(ctx, "Received signal, shutting down")
}
ctx, cancel = context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := server.Close(ctx); err != nil {
logger.Error(ctx, "failed to close server, err: %v", err)
}

logger.Info(ctx, "server shutdown")
logger.Info(ctx, "Redis is now ready to exit, bye bye...")
return nil
}

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ require (
github.com/redis/go-redis/v9 v9.5.3 // indirect
github.com/stretchr/testify v1.9.0 // indirect
github.com/test-go/testify v1.1.4 // indirect
github.com/zhuyie/golzf v0.0.0-20161112031142-8387b0307ade // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/test-go/testify v1.1.4 h1:Tf9lntrKUMHiXQ07qBScBTSA0dhYQlu83hswqelv1iE=
github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmNQzk2ghU=
github.com/zhuyie/golzf v0.0.0-20161112031142-8387b0307ade h1:bafvQukPrIYwYWcft4rl3WpHo3qO0/voaAgnCwgdhi0=
github.com/zhuyie/golzf v0.0.0-20161112031142-8387b0307ade/go.mod h1:juNhYdla04C276MyU4zR0BA7t90ziLKPwkjDgddGYV0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
2 changes: 1 addition & 1 deletion internal/app/server/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func (c *Config) Get(key string) (string, error) {
seachKey := strings.ToUpper(key)
value, exists := c.options[seachKey]
if !exists {
return "", ErrOptionNotFound
return "", fmt.Errorf("%s for %s", ErrOptionNotFound, key)
}
return value, nil
}
Expand Down
45 changes: 43 additions & 2 deletions internal/app/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import (
"context"
"fmt"
"net"
"os"
"sync"

"github.com/codecrafters-io/redis-starter-go/internal/app/server/config"
"github.com/codecrafters-io/redis-starter-go/internal/client"
"github.com/codecrafters-io/redis-starter-go/pkg/command"
"github.com/codecrafters-io/redis-starter-go/pkg/keyval"
"github.com/codecrafters-io/redis-starter-go/pkg/rdb"
"github.com/codecrafters-io/redis-starter-go/pkg/resp"
"github.com/codecrafters-io/redis-starter-go/pkg/telemetry/logger"
)
Expand All @@ -23,31 +25,67 @@ type Server struct {
mu sync.Mutex
cfg *config.Config
done chan struct{}
store keyval.KV
clients map[*client.Client]struct{}
cFactory *command.CommandFactory
}

func NewServer(cfg *config.Config) *Server {
store := keyval.NewStore()
return &Server{
mu: sync.Mutex{},
cfg: cfg,
done: make(chan struct{}),
cFactory: command.NewCommandFactory(keyval.NewStore(), cfg),
store: store,
cFactory: command.NewCommandFactory(store, cfg),
clients: make(map[*client.Client]struct{}),
}
}

func (s *Server) Start(ctx context.Context) error {
logger.Info(ctx, "Server initialized")
dir, _ := s.cfg.Get(config.DirKey)
dbFilename, _ := s.cfg.Get(config.DBFilenameKey)
file, openErr := os.Open(dir + "/" + dbFilename)
if openErr != nil && !os.IsNotExist(openErr) {
return fmt.Errorf("failed to open file, err: %v", openErr)
}
var isValidRDBFile bool
var stat os.FileInfo
var err error
if !os.IsNotExist(openErr) {
stat, err = file.Stat()
if err != nil {
return fmt.Errorf("failed to get file stat, err: %v", err)
}
isValidRDBFile = !stat.IsDir()
}

if isValidRDBFile {
defer file.Close()
rdb := rdb.NewRDBParser(file)
if err := rdb.ParseRDB(ctx); err != nil {
return fmt.Errorf("failed to parse rdb, err: %v", err)
}
s.store.RestoreRDB(rdb.GetData(), rdb.GetExpiry())
}
if err := s.Listen(ctx); err != nil {
return fmt.Errorf("failed to listen, err: %v", err)
}
return nil
}

func (s *Server) Listen(ctx context.Context) error {
listenAddr, err := s.cfg.Get(config.ListenAddrKey)
if err != nil {
listenAddr = defaultListenAddr
}
ln, err := net.Listen("tcp", listenAddr)
logger.Info(ctx, "listening, addr: %s", ln.Addr())
if err != nil {
return err
}
s.ln = ln
logger.Info(ctx, "Ready to accept connections tcp on %s", listenAddr)
return s.loop(ctx)
}

Expand Down Expand Up @@ -127,6 +165,9 @@ func (s *Server) handleMessage(ctx context.Context, cl *client.Client, r *resp.R

func (s *Server) Close(_ context.Context) error {
close(s.done)
if s.ln == nil {
return nil
}
if err := s.ln.Close(); err != nil {
return fmt.Errorf("failed to close listener: %w", err)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/app/server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func startServer(ctx context.Context) (*Server, int, error) {
}
srv := NewServer(cfg)
go func() {
if err := srv.Listen(ctx); err != nil {
if err := srv.Start(ctx); err != nil {
panic(err)
}
}()
Expand Down
3 changes: 2 additions & 1 deletion pkg/command/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func (ec *EchoCommand) Execute(c *client.Client, wr *resp.Writer, args []*resp.R
return wr.WriteError(errors.New("wrong number of arguments for 'echo' command"))
}

return wr.WriteValue(args[0].Bytes())
return wr.WriteBytes(args[0].Bytes())
}

type PingCommand struct {
Expand All @@ -55,6 +55,7 @@ func NewCommandFactory(kv keyval.KV, cfg *config.Config) *CommandFactory {
"info": &Info{},
"client": &ClientCmd{},
"config": &ConfigCmd{cfg: cfg},
"keys": &Keys{kv: kv},
},
}
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/command/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,11 @@ func (c *ConfigCmd) handleGet(cl *client.Client, wr *resp.Writer, args []*resp.R
}
return wr.WriteMap(resp)
} else {
var resp []any
var resp []string
for key, value := range valueMap {
resp = append(resp, key)
resp = append(resp, value)
}
return wr.WriteArray(resp)
return wr.WriteStringSlice(resp)
}
}
2 changes: 1 addition & 1 deletion pkg/command/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,5 @@ func (g *Get) Execute(c *client.Client, wr *resp.Writer, args []*resp.Resp) erro
return wr.WriteNull(resp.BulkString)
}

return wr.WriteValue(val)
return wr.WriteBytes(val)
}
2 changes: 1 addition & 1 deletion pkg/command/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func (h *Info) Execute(c *client.Client, wr *resp.Writer, args []*resp.Resp) err
str := strings.Builder{}
if argsLen == 1 {
str.WriteString(buildSectionString(args[0].String(), sections[args[0].String()]))
return wr.WriteValue(str.String())
return wr.WriteString(str.String())
}
for sectionName, section := range sections {
str.WriteString(buildSectionString(sectionName, section))
Expand Down
36 changes: 36 additions & 0 deletions pkg/command/keys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package command

import (
"errors"

"github.com/codecrafters-io/redis-starter-go/internal/client"
"github.com/codecrafters-io/redis-starter-go/pkg/keyval"
"github.com/codecrafters-io/redis-starter-go/pkg/resp"
)

type Keys struct {
kv keyval.KV
}

func (k *Keys) Execute(_ *client.Client, wr *resp.Writer, args []*resp.Resp) error {
if len(args) != 1 {
return wr.WriteError(errors.New("wrong number of arguments for 'keys' command"))
}
pattern := args[0].String()
keys := k.kv.Keys()
if pattern == "*" {
return wr.WriteStringSlice(k.kv.Keys())
}
re, err := resp.TranslatePattern(pattern)
if err != nil {
return wr.WriteError(err)
}
var matchedKeys []string
for _, key := range keys {
if re.MatchString(key) {
matchedKeys = append(matchedKeys, key)
}
}

return wr.WriteStringSlice(matchedKeys)
}
4 changes: 2 additions & 2 deletions pkg/command/set.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func (s *Set) Execute(c *client.Client, wr *resp.Writer, args []*resp.Resp) erro

if opts.NX {
if opts.GET && keyExists {
return wr.WriteValue(oldVal)
return wr.WriteBytes(oldVal)
}
if keyExists {
return wr.WriteNull(resp.BulkString)
Expand Down Expand Up @@ -97,7 +97,7 @@ func (s *Set) Execute(c *client.Client, wr *resp.Writer, args []*resp.Resp) erro

if opts.GET {
if keyExists {
return wr.WriteValue(oldVal)
return wr.WriteBytes(oldVal)
} else {
return wr.WriteNull(resp.BulkString)
}
Expand Down
64 changes: 64 additions & 0 deletions pkg/crc64/crc64.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package crc64 implements the Jones coefficients with an init value of 0.
package crc64

import "hash"

// Redis uses the CRC64 variant with "Jones" coefficients and init value of 0.
//
// Specification of this CRC64 variant follows:
// Name: crc-64-jones
// Width: 64 bits
// Poly: 0xad93d23594c935a9
// Reflected In: True
// Xor_In: 0xffffffffffffffff
// Reflected_Out: True
// Xor_Out: 0x0

var table = [256]uint64{0x0000000000000000, 0x7ad870c830358979, 0xf5b0e190606b12f2, 0x8f689158505e9b8b, 0xc038e5739841b68f, 0xbae095bba8743ff6, 0x358804e3f82aa47d, 0x4f50742bc81f2d04, 0xab28ecb46814fe75, 0xd1f09c7c5821770c, 0x5e980d24087fec87, 0x24407dec384a65fe, 0x6b1009c7f05548fa, 0x11c8790fc060c183, 0x9ea0e857903e5a08, 0xe478989fa00bd371, 0x7d08ff3b88be6f81, 0x07d08ff3b88be6f8, 0x88b81eabe8d57d73, 0xf2606e63d8e0f40a, 0xbd301a4810ffd90e, 0xc7e86a8020ca5077, 0x4880fbd87094cbfc, 0x32588b1040a14285, 0xd620138fe0aa91f4, 0xacf86347d09f188d, 0x2390f21f80c18306, 0x594882d7b0f40a7f, 0x1618f6fc78eb277b, 0x6cc0863448deae02, 0xe3a8176c18803589, 0x997067a428b5bcf0, 0xfa11fe77117cdf02, 0x80c98ebf2149567b, 0x0fa11fe77117cdf0, 0x75796f2f41224489, 0x3a291b04893d698d, 0x40f16bccb908e0f4, 0xcf99fa94e9567b7f, 0xb5418a5cd963f206, 0x513912c379682177, 0x2be1620b495da80e, 0xa489f35319033385, 0xde51839b2936bafc, 0x9101f7b0e12997f8, 0xebd98778d11c1e81, 0x64b116208142850a, 0x1e6966e8b1770c73, 0x8719014c99c2b083, 0xfdc17184a9f739fa, 0x72a9e0dcf9a9a271, 0x08719014c99c2b08, 0x4721e43f0183060c, 0x3df994f731b68f75, 0xb29105af61e814fe, 0xc849756751dd9d87, 0x2c31edf8f1d64ef6, 0x56e99d30c1e3c78f, 0xd9810c6891bd5c04, 0xa3597ca0a188d57d, 0xec09088b6997f879, 0x96d1784359a27100, 0x19b9e91b09fcea8b, 0x636199d339c963f2, 0xdf7adabd7a6e2d6f, 0xa5a2aa754a5ba416, 0x2aca3b2d1a053f9d, 0x50124be52a30b6e4, 0x1f423fcee22f9be0, 0x659a4f06d21a1299, 0xeaf2de5e82448912, 0x902aae96b271006b, 0x74523609127ad31a, 0x0e8a46c1224f5a63, 0x81e2d7997211c1e8, 0xfb3aa75142244891, 0xb46ad37a8a3b6595, 0xceb2a3b2ba0eecec, 0x41da32eaea507767, 0x3b024222da65fe1e, 0xa2722586f2d042ee, 0xd8aa554ec2e5cb97, 0x57c2c41692bb501c, 0x2d1ab4dea28ed965, 0x624ac0f56a91f461, 0x1892b03d5aa47d18, 0x97fa21650afae693, 0xed2251ad3acf6fea, 0x095ac9329ac4bc9b, 0x7382b9faaaf135e2, 0xfcea28a2faafae69, 0x8632586aca9a2710, 0xc9622c4102850a14, 0xb3ba5c8932b0836d, 0x3cd2cdd162ee18e6, 0x460abd1952db919f, 0x256b24ca6b12f26d, 0x5fb354025b277b14, 0xd0dbc55a0b79e09f, 0xaa03b5923b4c69e6, 0xe553c1b9f35344e2, 0x9f8bb171c366cd9b, 0x10e3202993385610, 0x6a3b50e1a30ddf69, 0x8e43c87e03060c18, 0xf49bb8b633338561, 0x7bf329ee636d1eea, 0x012b592653589793, 0x4e7b2d0d9b47ba97, 0x34a35dc5ab7233ee, 0xbbcbcc9dfb2ca865, 0xc113bc55cb19211c, 0x5863dbf1e3ac9dec, 0x22bbab39d3991495, 0xadd33a6183c78f1e, 0xd70b4aa9b3f20667, 0x985b3e827bed2b63, 0xe2834e4a4bd8a21a, 0x6debdf121b863991, 0x1733afda2bb3b0e8, 0xf34b37458bb86399, 0x8993478dbb8deae0, 0x06fbd6d5ebd3716b, 0x7c23a61ddbe6f812, 0x3373d23613f9d516, 0x49aba2fe23cc5c6f, 0xc6c333a67392c7e4, 0xbc1b436e43a74e9d, 0x95ac9329ac4bc9b5, 0xef74e3e19c7e40cc, 0x601c72b9cc20db47, 0x1ac40271fc15523e, 0x5594765a340a7f3a, 0x2f4c0692043ff643, 0xa02497ca54616dc8, 0xdafce7026454e4b1, 0x3e847f9dc45f37c0, 0x445c0f55f46abeb9, 0xcb349e0da4342532, 0xb1eceec59401ac4b, 0xfebc9aee5c1e814f, 0x8464ea266c2b0836, 0x0b0c7b7e3c7593bd, 0x71d40bb60c401ac4, 0xe8a46c1224f5a634, 0x927c1cda14c02f4d, 0x1d148d82449eb4c6, 0x67ccfd4a74ab3dbf, 0x289c8961bcb410bb, 0x5244f9a98c8199c2, 0xdd2c68f1dcdf0249, 0xa7f41839ecea8b30, 0x438c80a64ce15841, 0x3954f06e7cd4d138, 0xb63c61362c8a4ab3, 0xcce411fe1cbfc3ca, 0x83b465d5d4a0eece, 0xf96c151de49567b7, 0x76048445b4cbfc3c, 0x0cdcf48d84fe7545, 0x6fbd6d5ebd3716b7, 0x15651d968d029fce, 0x9a0d8ccedd5c0445, 0xe0d5fc06ed698d3c, 0xaf85882d2576a038, 0xd55df8e515432941, 0x5a3569bd451db2ca, 0x20ed197575283bb3, 0xc49581ead523e8c2, 0xbe4df122e51661bb, 0x3125607ab548fa30, 0x4bfd10b2857d7349, 0x04ad64994d625e4d, 0x7e7514517d57d734, 0xf11d85092d094cbf, 0x8bc5f5c11d3cc5c6, 0x12b5926535897936, 0x686de2ad05bcf04f, 0xe70573f555e26bc4, 0x9ddd033d65d7e2bd, 0xd28d7716adc8cfb9, 0xa85507de9dfd46c0, 0x273d9686cda3dd4b, 0x5de5e64efd965432, 0xb99d7ed15d9d8743, 0xc3450e196da80e3a, 0x4c2d9f413df695b1, 0x36f5ef890dc31cc8, 0x79a59ba2c5dc31cc, 0x037deb6af5e9b8b5, 0x8c157a32a5b7233e, 0xf6cd0afa9582aa47, 0x4ad64994d625e4da, 0x300e395ce6106da3, 0xbf66a804b64ef628, 0xc5bed8cc867b7f51, 0x8aeeace74e645255, 0xf036dc2f7e51db2c, 0x7f5e4d772e0f40a7, 0x05863dbf1e3ac9de, 0xe1fea520be311aaf, 0x9b26d5e88e0493d6, 0x144e44b0de5a085d, 0x6e963478ee6f8124, 0x21c640532670ac20, 0x5b1e309b16452559, 0xd476a1c3461bbed2, 0xaeaed10b762e37ab, 0x37deb6af5e9b8b5b, 0x4d06c6676eae0222, 0xc26e573f3ef099a9, 0xb8b627f70ec510d0, 0xf7e653dcc6da3dd4, 0x8d3e2314f6efb4ad, 0x0256b24ca6b12f26, 0x788ec2849684a65f, 0x9cf65a1b368f752e, 0xe62e2ad306bafc57, 0x6946bb8b56e467dc, 0x139ecb4366d1eea5, 0x5ccebf68aecec3a1, 0x2616cfa09efb4ad8, 0xa97e5ef8cea5d153, 0xd3a62e30fe90582a, 0xb0c7b7e3c7593bd8, 0xca1fc72bf76cb2a1, 0x45775673a732292a, 0x3faf26bb9707a053, 0x70ff52905f188d57, 0x0a2722586f2d042e, 0x854fb3003f739fa5, 0xff97c3c80f4616dc, 0x1bef5b57af4dc5ad, 0x61372b9f9f784cd4, 0xee5fbac7cf26d75f, 0x9487ca0fff135e26, 0xdbd7be24370c7322, 0xa10fceec0739fa5b, 0x2e675fb4576761d0, 0x54bf2f7c6752e8a9, 0xcdcf48d84fe75459, 0xb71738107fd2dd20, 0x387fa9482f8c46ab, 0x42a7d9801fb9cfd2, 0x0df7adabd7a6e2d6, 0x772fdd63e7936baf, 0xf8474c3bb7cdf024, 0x829f3cf387f8795d, 0x66e7a46c27f3aa2c, 0x1c3fd4a417c62355, 0x935745fc4798b8de, 0xe98f353477ad31a7, 0xa6df411fbfb21ca3, 0xdc0731d78f8795da, 0x536fa08fdfd90e51, 0x29b7d047efec8728}

func crc64(crc uint64, b []byte) uint64 {
for _, v := range b {
crc = table[byte(crc)^v] ^ (crc >> 8)
}
return crc
}

func Digest(b []byte) uint64 {
return crc64(0, b)
}

type digest struct {
crc uint64
}

func New() hash.Hash64 {
return &digest{}
}

func (h *digest) Write(p []byte) (int, error) {
h.crc = crc64(h.crc, p)
return len(p), nil
}

// Encode in little endian
func (d *digest) Sum(in []byte) []byte {
s := d.Sum64()
in = append(in, byte(s))
in = append(in, byte(s>>8))
in = append(in, byte(s>>16))
in = append(in, byte(s>>24))
in = append(in, byte(s>>32))
in = append(in, byte(s>>40))
in = append(in, byte(s>>48))
in = append(in, byte(s>>56))
return in
}

func (d *digest) Sum64() uint64 { return d.crc }
func (d *digest) BlockSize() int { return 1 }
func (d *digest) Size() int { return 8 }
func (d *digest) Reset() { d.crc = 0 }
23 changes: 23 additions & 0 deletions pkg/keyval/kv.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
)

type KV interface {
RestoreRDB(data map[string]string, expiry map[string]uint64)
Get(key string) ([]byte, error)
Set(key string, value []byte) error
Expire(key string, duration time.Duration)
Expand All @@ -14,6 +15,7 @@ type KV interface {
PExpireAt(key string, expiry time.Time)
Exists(key string) bool
DeleteTTL(key string)
Keys() []string
}

type kv struct {
Expand Down Expand Up @@ -58,6 +60,27 @@ func (kv *kv) Expire(key string, duration time.Duration) {
kv.expiry[key] = time.Now().Add(duration)
}

func (kv *kv) Keys() []string {
kv.mu.RLock()
defer kv.mu.RUnlock()
keys := make([]string, 0, len(kv.store))
for key := range kv.store {
keys = append(keys, key)
}
return keys
}

func (kv *kv) RestoreRDB(data map[string]string, expiry map[string]uint64) {
kv.mu.Lock()
defer kv.mu.Unlock()
for key, value := range data {
kv.store[key] = []byte(value)
if exp, ok := expiry[key]; ok {
kv.expiry[key] = time.UnixMilli(int64(exp))
}
}
}

func (kv *kv) PExpire(key string, duration time.Duration) {
kv.Expire(key, duration)
}
Expand Down
Loading

0 comments on commit 2538a42

Please sign in to comment.