├── example ├── bin │ └── ggcache ├── Makefile ├── proto │ ├── protocol_test.go │ └── protocol.go ├── main.go ├── client │ └── client.go └── server.go ├── go.mod ├── go.sum ├── LICENSE ├── README.md ├── cache_test.go └── cache.go /example/bin/ggcache: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthdm/ggcache/HEAD/example/bin/ggcache -------------------------------------------------------------------------------- /example/Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | go build -o bin/ggcache 3 | 4 | run: build 5 | ./bin/ggcache 6 | 7 | runfollower: build 8 | ./bin/ggcache --listenaddr :4000 --leaderaddr :3000 9 | 10 | test: 11 | @go test -v ./... -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/anthdm/ggcache 2 | 3 | go 1.21.2 4 | 5 | require github.com/stretchr/testify v1.8.4 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.1 // indirect 9 | github.com/pmezard/go-difflib v1.0.0 // indirect 10 | gopkg.in/yaml.v3 v3.0.1 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 6 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 7 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 9 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 10 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 11 | -------------------------------------------------------------------------------- /example/proto/protocol_test.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestParseGetCommand(t *testing.T) { 11 | cmd := &CommandGet{ 12 | Key: []byte("Foo"), 13 | } 14 | r := bytes.NewReader(cmd.Bytes()) 15 | pcmd, err := ParseCommand(r) 16 | assert.Nil(t, err) 17 | 18 | assert.Equal(t, cmd, pcmd) 19 | } 20 | 21 | func TestParseSetCommand(t *testing.T) { 22 | cmd := &CommandSet{ 23 | Key: []byte("Foo"), 24 | Value: []byte("Bar"), 25 | TTL: 2, 26 | } 27 | r := bytes.NewReader(cmd.Bytes()) 28 | pcmd, err := ParseCommand(r) 29 | assert.Nil(t, err) 30 | 31 | assert.Equal(t, cmd, pcmd) 32 | } 33 | 34 | func BenchmarkParseCommand(b *testing.B) { 35 | cmd := &CommandSet{ 36 | Key: []byte("Foo"), 37 | Value: []byte("Bar"), 38 | TTL: 2, 39 | } 40 | 41 | for i := 0; i < b.N; i++ { 42 | r := bytes.NewReader(cmd.Bytes()) 43 | _, _ = ParseCommand(r) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 @anthdm and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "time" 9 | 10 | "github.com/anthdm/ggcache" 11 | "github.com/anthdm/ggcache/example/client" 12 | ) 13 | 14 | func main() { 15 | var ( 16 | listenAddr = flag.String("listenaddr", ":3000", "listen address of the server") 17 | leaderAddr = flag.String("leaderaddr", "", "listen address of the leader") 18 | ) 19 | flag.Parse() 20 | 21 | opts := ServerOpts{ 22 | ListenAddr: *listenAddr, 23 | IsLeader: len(*leaderAddr) == 0, 24 | LeaderAddr: *leaderAddr, 25 | } 26 | 27 | go func() { 28 | time.Sleep(time.Second * 10) 29 | if opts.IsLeader { 30 | SendStuff() 31 | } 32 | }() 33 | 34 | server := NewServer(opts, ggcache.New()) 35 | _ = server.Start() 36 | } 37 | 38 | func SendStuff() { 39 | for i := 0; i < 100; i++ { 40 | go func(i int) { 41 | c, err := client.New(":3000", client.Options{}) 42 | if err != nil { 43 | log.Fatal(err) 44 | } 45 | 46 | var ( 47 | key = []byte(fmt.Sprintf("key_%d", i)) 48 | value = []byte(fmt.Sprintf("val_%d", i)) 49 | ) 50 | 51 | err = c.Set(context.Background(), key, value, 0) 52 | if err != nil { 53 | log.Fatal(err) 54 | } 55 | 56 | fetchedValue, err := c.Get(context.Background(), key) 57 | if err != nil { 58 | log.Fatal(err) 59 | } 60 | fmt.Println(string(fetchedValue)) 61 | 62 | _ = c.Close() 63 | }(i) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /example/client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | 8 | "github.com/anthdm/ggcache/example/proto" 9 | ) 10 | 11 | type Options struct{} 12 | 13 | type Client struct { 14 | conn net.Conn 15 | } 16 | 17 | func NewFromConn(conn net.Conn) *Client { 18 | return &Client{ 19 | conn: conn, 20 | } 21 | } 22 | 23 | func New(endpoint string, _ Options) (*Client, error) { 24 | conn, err := net.Dial("tcp", endpoint) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | return &Client{ 30 | conn: conn, 31 | }, nil 32 | } 33 | 34 | func (c *Client) Get(_ context.Context, key []byte) ([]byte, error) { 35 | cmd := &proto.CommandGet{ 36 | Key: key, 37 | } 38 | 39 | _, err := c.conn.Write(cmd.Bytes()) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | resp, err := proto.ParseGetResponse(c.conn) 45 | if err != nil { 46 | return nil, err 47 | } 48 | if resp.Status == proto.StatusKeyNotFound { 49 | return nil, fmt.Errorf("could not find key (%s)", key) 50 | } 51 | if resp.Status != proto.StatusOK { 52 | return nil, fmt.Errorf("server responded with non OK status [%s]", resp.Status) 53 | } 54 | 55 | return resp.Value, nil 56 | } 57 | 58 | func (c *Client) Set(_ context.Context, key []byte, value []byte, ttl int) error { 59 | cmd := &proto.CommandSet{ 60 | Key: key, 61 | Value: value, 62 | TTL: ttl, 63 | } 64 | 65 | _, err := c.conn.Write(cmd.Bytes()) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | resp, err := proto.ParseSetResponse(c.conn) 71 | if err != nil { 72 | return err 73 | } 74 | if resp.Status != proto.StatusOK { 75 | return fmt.Errorf("server responsed with non OK status [%s]", resp.Status) 76 | } 77 | 78 | return nil 79 | } 80 | 81 | func (c *Client) Close() error { 82 | return c.conn.Close() 83 | } 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ggcache 2 | 3 | `ggcache` is a flexible and lightweight in-memory cache package for Go, suitable for both single-cache applications and distributed systems with multiple clients. 4 | 5 | ## Overview 6 | 7 | This package provides a basic in-memory caching mechanism with support for key-value storage, expiration time, and concurrent read and write safety. It is designed to be versatile and adaptable to various use cases, from standalone applications to distributed systems where multiple clients share a common cache. 8 | 9 | ## Features 10 | 11 | - **Concurrent Safety:** The cache utilizes a `sync.RWMutex` to ensure safe access and modification of data in a concurrent environment. 12 | 13 | - **Expiration Time:** Cached items can have an optional expiration time, allowing automatic removal of entries after a specified duration. 14 | 15 | - **Client-Server Architecture:** The package supports a client-server architecture, enabling multiple clients to interact with a centralized cache server. 16 | 17 | ## Installation 18 | 19 | To use `ggcache` in your Go project, you can install it using the following: 20 | 21 | ```bash 22 | go get github.com/anthdm/ggcache 23 | ``` 24 | 25 | ## Single Cache Example 26 | ```go 27 | package main 28 | 29 | import ( 30 | "fmt" 31 | "time" 32 | 33 | "github.com/anthdm/ggcache" 34 | ) 35 | 36 | func main() { 37 | // Create a new cache instance 38 | cache := ggcache.New() 39 | 40 | // Set a key-value pair in the cache 41 | key := []byte("exampleKey") 42 | value := []byte("exampleValue") 43 | err := cache.Set(key, value, time.Second*30) 44 | if err != nil { 45 | fmt.Println("Error:", err) 46 | } 47 | 48 | // Get the value from the cache 49 | result, err := cache.Get(key) 50 | if err != nil { 51 | fmt.Println("Error:", err) 52 | } else { 53 | fmt.Println("Value:", string(result)) 54 | } 55 | 56 | // Check if a key exists in the cache 57 | if cache.Has(key) { 58 | fmt.Println("Key exists in the cache.") 59 | } 60 | 61 | // Delete a key from the cache 62 | err = cache.Delete(key) 63 | if err != nil { 64 | fmt.Println("Error:", err) 65 | } 66 | } 67 | ``` 68 | 69 | ## Contributing 70 | Feel free to contribute to the development of ggcache by submitting issues or pull requests. 71 | 72 | ## License 73 | This package is licensed under the MIT License - see the LICENSE file for details. 74 | -------------------------------------------------------------------------------- /example/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/binary" 6 | "fmt" 7 | "io" 8 | "log" 9 | "net" 10 | "time" 11 | 12 | "github.com/anthdm/ggcache" 13 | "github.com/anthdm/ggcache/example/client" 14 | "github.com/anthdm/ggcache/example/proto" 15 | ) 16 | 17 | type ServerOpts struct { 18 | ListenAddr string 19 | IsLeader bool 20 | LeaderAddr string 21 | } 22 | 23 | type Server struct { 24 | ServerOpts 25 | 26 | members map[*client.Client]struct{} 27 | 28 | cache ggcache.Cacher 29 | } 30 | 31 | func NewServer(opts ServerOpts, c ggcache.Cacher) *Server { 32 | return &Server{ 33 | ServerOpts: opts, 34 | cache: c, 35 | members: make(map[*client.Client]struct{}), 36 | } 37 | } 38 | 39 | func (s *Server) Start() error { 40 | ln, err := net.Listen("tcp", s.ListenAddr) 41 | if err != nil { 42 | return fmt.Errorf("listen error: %s", err) 43 | } 44 | 45 | if !s.IsLeader && len(s.LeaderAddr) != 0 { 46 | go func() { 47 | if err := s.dialLeader(); err != nil { 48 | log.Println(err) 49 | } 50 | }() 51 | } 52 | 53 | log.Printf("server starting on port [%s]\n", s.ListenAddr) 54 | 55 | for { 56 | conn, err := ln.Accept() 57 | if err != nil { 58 | log.Printf("accept error: %s\n", err) 59 | continue 60 | } 61 | go s.handleConn(conn) 62 | } 63 | } 64 | 65 | func (s *Server) dialLeader() error { 66 | conn, err := net.Dial("tcp", s.LeaderAddr) 67 | if err != nil { 68 | return fmt.Errorf("failed to dial leader [%s]", s.LeaderAddr) 69 | } 70 | 71 | log.Println("connected to leader:", s.LeaderAddr) 72 | 73 | if err = binary.Write(conn, binary.LittleEndian, proto.CmdJoin); err != nil { 74 | return err 75 | } 76 | 77 | s.handleConn(conn) 78 | 79 | return nil 80 | } 81 | 82 | func (s *Server) handleConn(conn net.Conn) { 83 | defer func(conn net.Conn) { 84 | _ = conn.Close() 85 | }(conn) 86 | 87 | //fmt.Println("connection made:", conn.RemoteAddr()) 88 | 89 | for { 90 | cmd, err := proto.ParseCommand(conn) 91 | if err != nil { 92 | if err == io.EOF { 93 | break 94 | } 95 | log.Println("parse command error:", err) 96 | break 97 | } 98 | go s.handleCommand(conn, cmd) 99 | } 100 | 101 | // fmt.Println("connection closed:", conn.RemoteAddr()) 102 | } 103 | 104 | func (s *Server) handleCommand(conn net.Conn, cmd any) { 105 | switch v := cmd.(type) { 106 | case *proto.CommandSet: 107 | _ = s.handleSetCommand(conn, v) 108 | case *proto.CommandGet: 109 | _ = s.handleGetCommand(conn, v) 110 | case *proto.CommandJoin: 111 | _ = s.handleJoinCommand(conn, v) 112 | } 113 | } 114 | 115 | func (s *Server) handleJoinCommand(conn net.Conn, _ *proto.CommandJoin) error { 116 | fmt.Println("member just joined the cluster:", conn.RemoteAddr()) 117 | 118 | s.members[client.NewFromConn(conn)] = struct{}{} 119 | 120 | return nil 121 | } 122 | 123 | func (s *Server) handleGetCommand(conn net.Conn, cmd *proto.CommandGet) error { 124 | // log.Printf("GET %s", cmd.Key) 125 | 126 | resp := proto.ResponseGet{} 127 | value, err := s.cache.Get(cmd.Key) 128 | if err != nil { 129 | resp.Status = proto.StatusError 130 | _, err := conn.Write(resp.Bytes()) 131 | return err 132 | } 133 | 134 | resp.Status = proto.StatusOK 135 | resp.Value = value 136 | _, err = conn.Write(resp.Bytes()) 137 | 138 | return err 139 | } 140 | 141 | func (s *Server) handleSetCommand(conn net.Conn, cmd *proto.CommandSet) error { 142 | log.Printf("SET %s to %s", cmd.Key, cmd.Value) 143 | 144 | go func() { 145 | for member := range s.members { 146 | err := member.Set(context.TODO(), cmd.Key, cmd.Value, cmd.TTL) 147 | if err != nil { 148 | log.Println("forward to member error:", err) 149 | } 150 | } 151 | }() 152 | 153 | resp := proto.ResponseSet{} 154 | if err := s.cache.Set(cmd.Key, cmd.Value, time.Duration(cmd.TTL)); err != nil { 155 | resp.Status = proto.StatusError 156 | _, err := conn.Write(resp.Bytes()) 157 | return err 158 | } 159 | 160 | resp.Status = proto.StatusOK 161 | _, err := conn.Write(resp.Bytes()) 162 | 163 | return err 164 | } 165 | -------------------------------------------------------------------------------- /cache_test.go: -------------------------------------------------------------------------------- 1 | package ggcache 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | // TestCache_Get tests the Get method of the Cache. 9 | func TestCache_Get(t *testing.T) { 10 | cache := New() 11 | 12 | // Test Case 1: Key not found 13 | _, err := cache.Get([]byte("nonexistent")) 14 | if err == nil { 15 | t.Error("Expected error for nonexistent key, but got nil") 16 | } 17 | 18 | // Test Case 2: Key found 19 | key := []byte("testKey") 20 | value := []byte("testValue") 21 | _ = cache.Set(key, value, 0) 22 | 23 | retrievedValue, err := cache.Get(key) 24 | if err != nil { 25 | t.Errorf("Unexpected error: %v", err) 26 | } 27 | 28 | if string(retrievedValue) != string(value) { 29 | t.Errorf("Expected value %s, but got %s", value, retrievedValue) 30 | } 31 | } 32 | 33 | // TestCache_Set tests the Set method of the Cache. 34 | func TestCache_Set(t *testing.T) { 35 | cache := New() 36 | 37 | // Test Case 1: Set with TTL 38 | key := []byte("testKey") 39 | value := []byte("testValue") 40 | ttl := time.Millisecond * 100 41 | err := cache.Set(key, value, ttl) 42 | if err != nil { 43 | t.Errorf("Unexpected error: %v", err) 44 | } 45 | 46 | // Wait for TTL to expire 47 | time.Sleep(ttl + time.Millisecond*50) 48 | 49 | // Check that key is not present in the cache 50 | if cache.Has(key) { 51 | t.Error("Expected key to be expired, but it's still present") 52 | } 53 | } 54 | 55 | // TestCache_Has tests the Has method of the Cache. 56 | func TestCache_Has(t *testing.T) { 57 | cache := New() 58 | 59 | // Test Case 1: Key not present 60 | if cache.Has([]byte("nonexistent")) { 61 | t.Error("Expected false for nonexistent key, but got true") 62 | } 63 | 64 | // Test Case 2: Key present 65 | key := []byte("testKey") 66 | value := []byte("testValue") 67 | _ = cache.Set(key, value, 0) 68 | 69 | if !cache.Has(key) { 70 | t.Error("Expected true for existing key, but got false") 71 | } 72 | } 73 | 74 | // TestCache_Delete tests the Delete method of the Cache. 75 | func TestCache_Delete(t *testing.T) { 76 | cache := New() 77 | 78 | // Test Case 1: Delete nonexistent key 79 | err := cache.Delete([]byte("nonexistent")) 80 | if err != nil { 81 | t.Errorf("Unexpected error: %v", err) 82 | } 83 | 84 | // Test Case 2: Delete existing key 85 | key := []byte("testKey") 86 | value := []byte("testValue") 87 | _ = cache.Set(key, value, 0) 88 | 89 | err = cache.Delete(key) 90 | if err != nil { 91 | t.Errorf("Unexpected error: %v", err) 92 | } 93 | 94 | // Check that key is not present in the cache 95 | if cache.Has(key) { 96 | t.Error("Expected key to be deleted, but it's still present") 97 | } 98 | } 99 | 100 | // TestCacheIntegration tests the Cache integration by combining multiple operations. 101 | func TestCacheIntegration(t *testing.T) { 102 | cache := New() 103 | 104 | // Test Case 1: Set and Get 105 | key := []byte("testKey") 106 | value := []byte("testValue") 107 | err := cache.Set(key, value, 0) 108 | if err != nil { 109 | t.Errorf("Unexpected error during Set: %v", err) 110 | } 111 | 112 | retrievedValue, err := cache.Get(key) 113 | if err != nil { 114 | t.Errorf("Unexpected error during Get: %v", err) 115 | } 116 | 117 | if string(retrievedValue) != string(value) { 118 | t.Errorf("Expected value %s, but got %s", value, retrievedValue) 119 | } 120 | 121 | // Test Case 2: Set with TTL 122 | ttl := time.Millisecond * 100 123 | err = cache.Set(key, value, ttl) 124 | if err != nil { 125 | t.Errorf("Unexpected error during Set with TTL: %v", err) 126 | } 127 | 128 | // Wait for TTL to expire 129 | time.Sleep(ttl + time.Millisecond*50) 130 | 131 | // Check that key is not present in the cache 132 | if cache.Has(key) { 133 | t.Error("Expected key to be expired, but it's still present") 134 | } 135 | 136 | // Test Case 3: Delete 137 | err = cache.Delete(key) 138 | if err != nil { 139 | t.Errorf("Unexpected error during Delete: %v", err) 140 | } 141 | 142 | // Check that key is not present in the cache 143 | if cache.Has(key) { 144 | t.Error("Expected key to be deleted, but it's still present") 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /example/proto/protocol.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | "io" 8 | ) 9 | 10 | type Status byte 11 | 12 | func (s Status) String() string { 13 | switch s { 14 | case StatusError: 15 | return "ERR" 16 | case StatusOK: 17 | return "OK" 18 | case StatusKeyNotFound: 19 | return "KEYNOTFOUND" 20 | default: 21 | return "NONE" 22 | } 23 | } 24 | 25 | const ( 26 | StatusNone Status = iota 27 | StatusOK 28 | StatusError 29 | StatusKeyNotFound 30 | ) 31 | 32 | type Command byte 33 | 34 | const ( 35 | CmdNonce Command = iota 36 | CmdSet 37 | CmdGet 38 | CmdDel 39 | CmdJoin 40 | ) 41 | 42 | type ResponseSet struct { 43 | Status Status 44 | } 45 | 46 | func (r ResponseSet) Bytes() []byte { 47 | buf := new(bytes.Buffer) 48 | _ = binary.Write(buf, binary.LittleEndian, r.Status) 49 | 50 | return buf.Bytes() 51 | } 52 | 53 | type ResponseGet struct { 54 | Status Status 55 | Value []byte 56 | } 57 | 58 | func (r *ResponseGet) Bytes() []byte { 59 | buf := new(bytes.Buffer) 60 | _ = binary.Write(buf, binary.LittleEndian, r.Status) 61 | 62 | valueLen := int32(len(r.Value)) 63 | _ = binary.Write(buf, binary.LittleEndian, valueLen) 64 | _ = binary.Write(buf, binary.LittleEndian, r.Value) 65 | 66 | return buf.Bytes() 67 | } 68 | 69 | func ParseSetResponse(r io.Reader) (*ResponseSet, error) { 70 | resp := &ResponseSet{} 71 | err := binary.Read(r, binary.LittleEndian, &resp.Status) 72 | return resp, err 73 | } 74 | 75 | func ParseGetResponse(r io.Reader) (*ResponseGet, error) { 76 | resp := &ResponseGet{} 77 | if err := binary.Read(r, binary.LittleEndian, &resp.Status); err != nil { 78 | return resp, err 79 | } 80 | 81 | var valueLen int32 82 | if err := binary.Read(r, binary.LittleEndian, &valueLen); err != nil { 83 | return resp, err 84 | } 85 | 86 | resp.Value = make([]byte, valueLen) 87 | if err := binary.Read(r, binary.LittleEndian, &resp.Value); err != nil { 88 | return resp, err 89 | } 90 | 91 | return resp, nil 92 | } 93 | 94 | type CommandJoin struct{} 95 | 96 | type CommandSet struct { 97 | Key []byte 98 | Value []byte 99 | TTL int 100 | } 101 | 102 | func (c *CommandSet) Bytes() []byte { 103 | buf := new(bytes.Buffer) 104 | _ = binary.Write(buf, binary.LittleEndian, CmdSet) 105 | 106 | keyLen := int32(len(c.Key)) 107 | _ = binary.Write(buf, binary.LittleEndian, keyLen) 108 | _ = binary.Write(buf, binary.LittleEndian, c.Key) 109 | 110 | valueLen := int32(len(c.Value)) 111 | _ = binary.Write(buf, binary.LittleEndian, valueLen) 112 | _ = binary.Write(buf, binary.LittleEndian, c.Value) 113 | 114 | _ = binary.Write(buf, binary.LittleEndian, int32(c.TTL)) 115 | 116 | return buf.Bytes() 117 | } 118 | 119 | type CommandGet struct { 120 | Key []byte 121 | } 122 | 123 | func (c *CommandGet) Bytes() []byte { 124 | buf := new(bytes.Buffer) 125 | _ = binary.Write(buf, binary.LittleEndian, CmdGet) 126 | 127 | keyLen := int32(len(c.Key)) 128 | _ = binary.Write(buf, binary.LittleEndian, keyLen) 129 | _ = binary.Write(buf, binary.LittleEndian, c.Key) 130 | 131 | return buf.Bytes() 132 | } 133 | 134 | func ParseCommand(r io.Reader) (any, error) { 135 | var cmd Command 136 | if err := binary.Read(r, binary.LittleEndian, &cmd); err != nil { 137 | return nil, err 138 | } 139 | 140 | switch cmd { 141 | case CmdSet: 142 | return parseSetCommand(r), nil 143 | case CmdGet: 144 | return parseGetCommand(r), nil 145 | case CmdJoin: 146 | return &CommandJoin{}, nil 147 | default: 148 | return nil, fmt.Errorf("invalid command") 149 | } 150 | } 151 | 152 | func parseSetCommand(r io.Reader) *CommandSet { 153 | cmd := &CommandSet{} 154 | 155 | var keyLen int32 156 | _ = binary.Read(r, binary.LittleEndian, &keyLen) 157 | cmd.Key = make([]byte, keyLen) 158 | _ = binary.Read(r, binary.LittleEndian, &cmd.Key) 159 | 160 | var valueLen int32 161 | _ = binary.Read(r, binary.LittleEndian, &valueLen) 162 | cmd.Value = make([]byte, valueLen) 163 | _ = binary.Read(r, binary.LittleEndian, &cmd.Value) 164 | 165 | var ttl int32 166 | _ = binary.Read(r, binary.LittleEndian, &ttl) 167 | cmd.TTL = int(ttl) 168 | 169 | return cmd 170 | } 171 | 172 | func parseGetCommand(r io.Reader) *CommandGet { 173 | cmd := &CommandGet{} 174 | 175 | var keyLen int32 176 | _ = binary.Read(r, binary.LittleEndian, &keyLen) 177 | cmd.Key = make([]byte, keyLen) 178 | _ = binary.Read(r, binary.LittleEndian, &cmd.Key) 179 | 180 | return cmd 181 | } 182 | -------------------------------------------------------------------------------- /cache.go: -------------------------------------------------------------------------------- 1 | package ggcache 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | // Cacher is an interface used for performing caching operations. 10 | // Applications can implement this interface to integrate different caching managers. 11 | type Cacher interface { 12 | // Get returns the value associated with the specified key. 13 | // If the key is not found or an error occurs, an error object is returned. 14 | Get(key []byte) ([]byte, error) 15 | 16 | // Set adds the value associated with the specified key to the cache with the specified expiration time. 17 | // If the duration is zero, the cache is held indefinitely. 18 | Set(key []byte, value []byte, expiration time.Duration) error 19 | 20 | // Has checks whether the specified key exists in the cache. 21 | Has(key []byte) bool 22 | 23 | // Delete removes the specified key from the cache. 24 | // If the key is not found, an error object is returned. 25 | Delete(key []byte) error 26 | } 27 | 28 | // Cache is a simple in-memory cache implementation. 29 | // It utilizes a sync.RWMutex for concurrent read and write safety. 30 | // The cache stores data as byte slices, using string keys for retrieval. 31 | type Cache struct { 32 | // lock is a sync.RWMutex to ensure concurrent read and write safety. 33 | lock sync.RWMutex 34 | 35 | // data is a map that stores byte slices with string keys for retrieval. 36 | data map[string][]byte 37 | } 38 | 39 | // New creates and returns a new instance of the Cache with initialized internal data. 40 | // The Cache is an in-memory cache implementation using a sync.RWMutex for concurrency safety. 41 | // The internal data is represented as a map with string keys and byte slice values. 42 | func New() *Cache { 43 | return &Cache{ 44 | data: make(map[string][]byte), 45 | } 46 | } 47 | 48 | // Get retrieves the value associated with the specified key from the cache. 49 | // It acquires a read lock to ensure concurrent safety during retrieval. 50 | // If the key is not found, an error is returned indicating the absence of the key. 51 | // The retrieved value and a nil error are returned if the key is present in the cache. 52 | func (c *Cache) Get(key []byte) ([]byte, error) { 53 | // Acquire a read lock to ensure concurrent safety during retrieval. 54 | c.lock.RLock() 55 | defer c.lock.RUnlock() 56 | 57 | // Convert the byte slice key to a string for map lookup. 58 | keyStr := string(key) 59 | 60 | // Retrieve the value associated with the key from the internal data map. 61 | val, ok := c.data[keyStr] 62 | if !ok { 63 | // Return an error if the key is not found. 64 | return nil, fmt.Errorf("key (%s) not found", keyStr) 65 | } 66 | 67 | // Return the retrieved value and a nil error if the key is present in the cache. 68 | return val, nil 69 | } 70 | 71 | // Set adds or updates the cache with the specified key-value pair. 72 | // It acquires a write lock to ensure concurrent safety during insertion. 73 | // If the time-to-live (TTL) duration is greater than zero, a goroutine is launched to remove the entry after the specified duration. 74 | // The key-value pair is stored in the cache, and if a TTL is set, the entry is automatically deleted after the specified duration. 75 | // The method returns nil, indicating a successful operation. 76 | func (c *Cache) Set(key, value []byte, ttl time.Duration) error { 77 | // Acquire a write lock to ensure concurrent safety during insertion. 78 | c.lock.Lock() 79 | defer c.lock.Unlock() 80 | 81 | // Convert the byte slice key to a string for map storage. 82 | keyStr := string(key) 83 | 84 | // Add or update the cache with the specified key-value pair. 85 | c.data[keyStr] = value 86 | 87 | // If TTL is greater than zero, launch a goroutine to remove the entry after the specified duration. 88 | if ttl > 0 { 89 | go func() { 90 | <-time.After(ttl) 91 | c.lock.Lock() 92 | defer c.lock.Unlock() 93 | delete(c.data, keyStr) 94 | }() 95 | } 96 | 97 | // Return nil, indicating a successful operation. 98 | return nil 99 | } 100 | 101 | // Has checks if the specified key exists in the cache. 102 | // It acquires a read lock to ensure concurrent safety during the lookup. 103 | // The method returns true if the key is found in the cache, and false otherwise. 104 | func (c *Cache) Has(key []byte) bool { 105 | // Acquire a read lock to ensure concurrent safety during the lookup. 106 | c.lock.RLock() 107 | defer c.lock.RUnlock() 108 | 109 | // Check if the key exists in the cache. 110 | _, ok := c.data[string(key)] 111 | 112 | // Return true if the key is found, and false otherwise. 113 | return ok 114 | } 115 | 116 | // Delete removes the specified key from the cache. 117 | // It acquires a write lock to ensure concurrent safety during deletion. 118 | // The method returns nil, indicating a successful deletion. 119 | func (c *Cache) Delete(key []byte) error { 120 | // Acquire a write lock to ensure concurrent safety during deletion. 121 | c.lock.Lock() 122 | defer c.lock.Unlock() 123 | 124 | // Remove the specified key from the cache. 125 | delete(c.data, string(key)) 126 | 127 | // Return nil, indicating a successful deletion. 128 | return nil 129 | } 130 | --------------------------------------------------------------------------------