├── .github └── workflows │ └── go.yml ├── .gitignore ├── LICENSE ├── README.md ├── api ├── api.go └── api_test.go ├── cache ├── cache.go └── cache_test.go ├── go.mod └── main.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: [push] 3 | jobs: 4 | 5 | build: 6 | name: Build 7 | runs-on: ubuntu-latest 8 | steps: 9 | 10 | - name: Set up Go 1.13 11 | uses: actions/setup-go@v1 12 | with: 13 | go-version: 1.13 14 | id: go 15 | 16 | - name: Check out code into the Go module directory 17 | uses: actions/checkout@v1 18 | 19 | - name: Get dependencies 20 | run: | 21 | go get -v -t -d ./... 22 | if [ -f Gopkg.toml ]; then 23 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 24 | dep ensure 25 | fi 26 | 27 | go test ./... 28 | go test -race ./... 29 | 30 | - name: Build 31 | run: go build -v . 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Andrew Healey 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⛷️ in-memory-cache-over-http 2 | 3 | > My blog post: [Cloning Memcached with Go](https://healeycodes.com/go/tutorial/beginners/showdev/2019/10/21/cloning-memcached-with-go.html) 4 | 5 |
6 | 7 | [![](https://github.com/healeycodes/in-memory-cache-over-http/workflows/Go/badge.svg)](https://github.com/healeycodes/in-memory-cache-over-http/actions?query=workflow%3AGo) 8 | 9 |
10 | 11 | An in-memory key/value cache server over HTTP with no dependencies. 12 | 13 | Keys and values are strings. Integer math can be applied in some situations (like Memcached does). 14 | 15 | The caching method is Least Recently Used (LRU). 16 | 17 |
18 | 19 | ### Install 20 | 21 | `go get healeycodes/in-memory-cache-over-http` 22 | 23 |
24 | 25 | ### Setup 26 | 27 | - Set your PORT environmental variable. 28 | - Set APP_ENV to `production` to turn off logging. 29 | - Set SIZE to limit the number of key/value pairs, (0 is default - unlimited). 30 | 31 | ```bash 32 | # Linux/macOS 33 | export PORT=8000 34 | export APP_ENV=production 35 | 36 | # Command Prompt 37 | set PORT=8000 38 | set APP_ENV=production 39 | 40 | # PowerShell 41 | $env:PORT = "8000" 42 | $env:APP_ENV = "production" 43 | ``` 44 | 45 | - Run 46 | 47 | `go run .\main.go` 48 | 49 | - Build 50 | 51 | `go build` 52 | 53 |
54 | 55 | ### Usage 56 | 57 | Adding an expire parameter is always optional. Not setting it, or setting it to zero means that the key will not expire. It uses **Unix time**. 58 | 59 | Example usage. 60 | 61 | Set `name` to be `Andrew` with an expire time of `01/01/2030 @ 12:00am (UTC)` 62 | 63 | GET `localhost:8000/set?key=name&value=Andrew&expire=1893456000` (204 status code) 64 | 65 | Retrieve the value located at `name`. 66 | 67 | GET `localhost:8000/get?key=name` (200 status code, body: `Andrew`) 68 | 69 |
70 | 71 | ### Methods 72 | 73 | #### Set (params: key, value, expire) `/set` 74 | 75 | Set a key/value. Existing will be overwritten. 76 | 77 |
78 | 79 | #### Get `/get` 80 | 81 | Get a value from a key. 82 | 83 |
84 | 85 | #### Delete (params: key) `/delete` 86 | 87 | Delete a key. 88 | 89 |
90 | 91 | #### CheckAndSet (params: key, value, expire, compare) `/checkandset` 92 | 93 | Set a key/value if the current value at that key matches the compare. 94 | 95 | If no existing key, set the key/value. 96 | 97 |
98 | 99 | #### Increment (params: key, value, expire) `/increment` 100 | 101 | Increment a value. Both the existing value and the new value amount should be integers. 102 | 103 | If no existing key, set the key/value. 104 | 105 |
106 | 107 | #### Decrement (params: key, value, expire) `/decrement` 108 | 109 | Decrement a value. Both the existing value and the new value amount should be integers. 110 | 111 | If no existing key, set the key/value. 112 | 113 |
114 | 115 | #### Append (params: key, value, expire) `/append` 116 | 117 | Concatenates the new value onto the existing. 118 | 119 | If no existing key, set the key/value. 120 | 121 |
122 | 123 | #### Prepend (params: key, value, expire) `/prepend` 124 | 125 | Concatenates the existing value onto the new value. 126 | 127 | If no existing key, set the key/value. 128 | 129 |
130 | 131 | #### Flush `/flush` 132 | 133 | Clear the cache. Delete all keys and values. 134 | 135 |
136 | 137 | #### Stats `/stats` 138 | 139 | Return statistics about the cache. 140 | 141 | Example. 142 | 143 | ```json 144 | { 145 | "keyCount": 1, 146 | "maxSize": 0 147 | } 148 | ``` 149 | 150 |
151 | 152 | ### Tests 153 | 154 | The majority of tests are integration tests that test API routes while checking the underlying cache. 155 | 156 | There are some unit tests for the cache. 157 | 158 |
159 | 160 | Run tests recursively. 161 | 162 | `go test ./...` 163 | 164 | Example output. 165 | 166 | ```bash 167 | ? healeycodes/in-memory-cache-over-http [no test files] 168 | ok healeycodes/in-memory-cache-over-http/api 0.527s 169 | ok healeycodes/in-memory-cache-over-http/cache 0.340s 170 | ``` 171 | 172 |
173 | 174 | 175 | ### Contributing 176 | 177 | Feel free to raise any issues and pull requests 👍 178 | 179 | There is no road map for this project. My main motivations were to learn more about Go! 180 | -------------------------------------------------------------------------------- /api/api.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | "strconv" 8 | "time" 9 | 10 | "healeycodes/in-memory-cache-over-http/cache" 11 | ) 12 | 13 | var s *cache.Store 14 | 15 | // Listen on PORT. Defaults to 8000 16 | func Listen() { 17 | size, _ := strconv.Atoi(getEnv("SIZE", "0")) 18 | new(size) 19 | setup() 20 | start() 21 | } 22 | 23 | // New store 24 | func new(size int) { 25 | s = cache.Service(size) 26 | } 27 | 28 | // Setup path handlers 29 | func setup() { 30 | http.HandleFunc("/get", handle(Get)) 31 | http.HandleFunc("/set", handle(Set)) 32 | http.HandleFunc("/delete", handle(Delete)) 33 | http.HandleFunc("/checkandset", handle(CheckAndSet)) 34 | http.HandleFunc("/increment", handle(Increment)) 35 | http.HandleFunc("/decrement", handle(Decrement)) 36 | http.HandleFunc("/append", handle(Append)) 37 | http.HandleFunc("/prepend", handle(Prepend)) 38 | http.HandleFunc("/stats", handle(Stats)) 39 | http.HandleFunc("/flush", handle(Flush)) 40 | } 41 | 42 | // Start http 43 | func start() { 44 | port := getEnv("PORT", ":8000") 45 | fmt.Println("Listening on", port) 46 | err := http.ListenAndServe(port, nil) 47 | if err != nil { 48 | panic(err) 49 | } 50 | } 51 | 52 | // Get a key from the store 53 | // Status code: 200 if present, else 404 54 | // e.g. ?key=foo 55 | func Get(w http.ResponseWriter, r *http.Request) { 56 | value, exist := s.Get(r.URL.Query().Get("key")) 57 | if !exist { 58 | http.Error(w, "", 404) 59 | return 60 | } 61 | w.Header().Set("content-type", "text/plain") 62 | w.Write([]byte(value)) 63 | } 64 | 65 | // Set a key in the store 66 | // Status code: 204 67 | func Set(w http.ResponseWriter, r *http.Request) { 68 | s.Set( 69 | r.URL.Query().Get("key"), 70 | r.URL.Query().Get("value"), 71 | getExpire(r.URL.Query().Get("expire"))) 72 | w.WriteHeader(http.StatusNoContent) 73 | } 74 | 75 | // Delete a key in the store 76 | // Status code: 204 77 | func Delete(w http.ResponseWriter, r *http.Request) { 78 | s.Delete(r.URL.Query().Get("key")) 79 | w.WriteHeader(http.StatusNoContent) 80 | } 81 | 82 | // CheckAndSet a key in the store if it matches the compare value 83 | // Status code: 204 if matches else 400 84 | func CheckAndSet(w http.ResponseWriter, r *http.Request) { 85 | if s.CheckAndSet( 86 | r.URL.Query().Get("key"), 87 | r.URL.Query().Get("value"), 88 | getExpire(r.URL.Query().Get("expire")), 89 | r.URL.Query().Get("compare")) == true { 90 | w.WriteHeader(http.StatusNoContent) 91 | return 92 | } 93 | w.WriteHeader(http.StatusBadRequest) 94 | } 95 | 96 | // Increment a key in the store by an amount. If key missing, set the amount 97 | // Status code: 204 if incrementable else 400 98 | func Increment(w http.ResponseWriter, r *http.Request) { 99 | if err := s.Increment( 100 | r.URL.Query().Get("key"), 101 | r.URL.Query().Get("value"), 102 | getExpire(r.URL.Query().Get("expire"))); err == nil { 103 | w.WriteHeader(http.StatusNoContent) 104 | return 105 | } 106 | w.WriteHeader(http.StatusBadRequest) 107 | } 108 | 109 | // Decrement a key in the store by an amount. If key missing, set the amount 110 | // Status code: 204 if decrementable else 400 111 | func Decrement(w http.ResponseWriter, r *http.Request) { 112 | if err := s.Decrement( 113 | r.URL.Query().Get("key"), 114 | r.URL.Query().Get("value"), 115 | getExpire(r.URL.Query().Get("expire"))); err == nil { 116 | w.WriteHeader(http.StatusNoContent) 117 | return 118 | } 119 | w.WriteHeader(http.StatusBadRequest) 120 | } 121 | 122 | // Append to a key in the store 123 | // Status code: 204 124 | func Append(w http.ResponseWriter, r *http.Request) { 125 | s.Append( 126 | r.URL.Query().Get("key"), 127 | r.URL.Query().Get("value"), 128 | getExpire(r.URL.Query().Get("expire"))) 129 | w.WriteHeader(http.StatusNoContent) 130 | } 131 | 132 | // Prepend to a key in the store 133 | // Status code: 204 134 | func Prepend(w http.ResponseWriter, r *http.Request) { 135 | s.Prepend( 136 | r.URL.Query().Get("key"), 137 | r.URL.Query().Get("value"), 138 | getExpire(r.URL.Query().Get("expire"))) 139 | w.WriteHeader(http.StatusNoContent) 140 | } 141 | 142 | // Flush all keys 143 | // Status code: 204 144 | func Flush(w http.ResponseWriter, r *http.Request) { 145 | s.Flush() 146 | w.WriteHeader(http.StatusNoContent) 147 | } 148 | 149 | // Stats of the cache 150 | // Status code: 200 151 | func Stats(w http.ResponseWriter, r *http.Request) { 152 | w.Header().Set("content-type", "application/json") 153 | w.Write([]byte(s.Stats())) 154 | } 155 | 156 | // Main middleware 157 | // Just some development logging for now 158 | func handle(f func(http.ResponseWriter, *http.Request)) func(w http.ResponseWriter, r *http.Request) { 159 | return func(w http.ResponseWriter, r *http.Request) { 160 | if getEnv("APP_ENV", "") != "production" { 161 | fmt.Println(time.Now(), r.URL) 162 | } 163 | f(w, r) 164 | } 165 | } 166 | 167 | // Safely get the expire, 0 if error 168 | func getExpire(attempt string) int { 169 | value, err := strconv.Atoi(attempt) 170 | if err != nil { 171 | return 0 172 | } 173 | return value 174 | } 175 | 176 | // Gets an ENV variable else returns fallback 177 | func getEnv(key, fallback string) string { 178 | if value, ok := os.LookupEnv(key); ok { 179 | return value 180 | } 181 | return fallback 182 | } 183 | -------------------------------------------------------------------------------- /api/api_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "net/url" 7 | "testing" 8 | ) 9 | 10 | func TestGetMiss(t *testing.T) { 11 | // Test cache miss 12 | new(100) 13 | 14 | param := make(url.Values) 15 | param["key"] = []string{"name"} 16 | req, err := http.NewRequest("GET", "/get?"+param.Encode(), nil) 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | 21 | rr := httptest.NewRecorder() 22 | handler := http.HandlerFunc(Get) 23 | 24 | handler.ServeHTTP(rr, req) 25 | 26 | if status := rr.Code; status != http.StatusNotFound { 27 | t.Errorf("Handler returned wrong status code: got %v want %v", 28 | status, http.StatusOK) 29 | } 30 | } 31 | 32 | func TestGetHit(t *testing.T) { 33 | // Test cache hit 34 | KEY := "name" 35 | VALUE := "Andrew" 36 | 37 | new(100) 38 | 39 | s.Set(KEY, VALUE, 0) 40 | 41 | param := make(url.Values) 42 | param["key"] = []string{KEY} 43 | req, err := http.NewRequest("GET", "/get?"+param.Encode(), nil) 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | 48 | rr := httptest.NewRecorder() 49 | handler := http.HandlerFunc(Get) 50 | handler.ServeHTTP(rr, req) 51 | 52 | if status := rr.Code; status != http.StatusOK { 53 | t.Errorf("Handler returned wrong status code: got %v want %v", 54 | status, http.StatusOK) 55 | } 56 | 57 | expected := VALUE 58 | if rr.Body.String() != expected { 59 | t.Errorf("Handler returned unexpected body: got %v want %v", 60 | rr.Body.String(), expected) 61 | } 62 | } 63 | 64 | func TestSet(t *testing.T) { 65 | KEY := "name" 66 | VALUE := "Alice" 67 | 68 | new(100) 69 | 70 | param := make(url.Values) 71 | param["key"] = []string{KEY} 72 | param["value"] = []string{VALUE} 73 | param["expire"] = []string{"0"} 74 | req, err := http.NewRequest("GET", "/set?"+param.Encode(), nil) 75 | if err != nil { 76 | t.Fatal(err) 77 | } 78 | 79 | rr := httptest.NewRecorder() 80 | handler := http.HandlerFunc(Set) 81 | handler.ServeHTTP(rr, req) 82 | 83 | if status := rr.Code; status != http.StatusNoContent { 84 | t.Errorf("Handler returned wrong status code: got %v want %v", 85 | status, http.StatusOK) 86 | } 87 | 88 | if curValue, ok := s.Get(KEY); ok == false && curValue == VALUE { 89 | t.Errorf("Value wasn't set in cache") 90 | } 91 | } 92 | 93 | func TestDelete(t *testing.T) { 94 | KEY := "name" 95 | VALUE := "Alice" 96 | 97 | new(100) 98 | s.Set(KEY, VALUE, 0) 99 | 100 | param := make(url.Values) 101 | param["key"] = []string{KEY} 102 | 103 | req, err := http.NewRequest("GET", "/delete?"+param.Encode(), nil) 104 | if err != nil { 105 | t.Fatal(err) 106 | } 107 | 108 | rr := httptest.NewRecorder() 109 | handler := http.HandlerFunc(Delete) 110 | handler.ServeHTTP(rr, req) 111 | 112 | if status := rr.Code; status != http.StatusNoContent { 113 | t.Errorf("Handler returned wrong status code: got %v want %v", 114 | status, http.StatusOK) 115 | } 116 | 117 | if _, ok := s.Get(KEY); ok == true { 118 | t.Errorf("Value wasn't deleted") 119 | } 120 | } 121 | 122 | func TestCheckAndSetFail(t *testing.T) { 123 | KEY := "name" 124 | ORIGVALUE := "Mary" 125 | VALUE := "Alice" 126 | COMPARE := "NotAlice" 127 | 128 | new(100) 129 | s.Set(KEY, ORIGVALUE, 0) 130 | 131 | param := make(url.Values) 132 | param["key"] = []string{KEY} 133 | param["value"] = []string{VALUE} 134 | param["expire"] = []string{"0"} 135 | param["compare"] = []string{COMPARE} 136 | req, err := http.NewRequest("GET", "/checkandset?"+param.Encode(), nil) 137 | if err != nil { 138 | t.Fatal(err) 139 | } 140 | 141 | rr := httptest.NewRecorder() 142 | handler := http.HandlerFunc(CheckAndSet) 143 | handler.ServeHTTP(rr, req) 144 | 145 | if status := rr.Code; status != http.StatusBadRequest { 146 | t.Errorf("Handler returned wrong status code: got %v want %v", 147 | status, http.StatusBadRequest) 148 | } 149 | 150 | if curValue, _ := s.Get(KEY); curValue != ORIGVALUE { 151 | t.Errorf("Value was set even though the compare should have failed") 152 | } 153 | } 154 | 155 | func TestCheckAndSetOk(t *testing.T) { 156 | KEY := "name" 157 | ORIGVALUE := "Mary" 158 | VALUE := "Alice" 159 | COMPARE := ORIGVALUE 160 | 161 | new(100) 162 | s.Set(KEY, ORIGVALUE, 0) 163 | 164 | param := make(url.Values) 165 | param["key"] = []string{KEY} 166 | param["value"] = []string{VALUE} 167 | param["expire"] = []string{"0"} 168 | param["compare"] = []string{COMPARE} 169 | req, err := http.NewRequest("GET", "/checkandset?"+param.Encode(), nil) 170 | if err != nil { 171 | t.Fatal(err) 172 | } 173 | 174 | rr := httptest.NewRecorder() 175 | handler := http.HandlerFunc(CheckAndSet) 176 | handler.ServeHTTP(rr, req) 177 | 178 | if status := rr.Code; status != http.StatusNoContent { 179 | t.Errorf("Handler returned wrong status code: got %v want %v", 180 | status, http.StatusNoContent) 181 | } 182 | 183 | if curValue, _ := s.Get(KEY); curValue == ORIGVALUE { 184 | t.Errorf("Value wasn't set even though the compare should have passed") 185 | } 186 | } 187 | 188 | func TestIncrement(t *testing.T) { 189 | KEY := "hits" 190 | VALUE := "1" 191 | ADDVALUE := "1" 192 | RESULT := "2" 193 | 194 | new(100) 195 | s.Set(KEY, VALUE, 0) 196 | 197 | param := make(url.Values) 198 | param["key"] = []string{KEY} 199 | param["value"] = []string{ADDVALUE} 200 | req, err := http.NewRequest("GET", "/increment?"+param.Encode(), nil) 201 | if err != nil { 202 | t.Fatal(err) 203 | } 204 | 205 | rr := httptest.NewRecorder() 206 | handler := http.HandlerFunc(Increment) 207 | handler.ServeHTTP(rr, req) 208 | 209 | if status := rr.Code; status != http.StatusNoContent { 210 | t.Errorf("Handler returned wrong status code: got %v want %v", 211 | status, http.StatusNoContent) 212 | } 213 | 214 | if curValue, _ := s.Get(KEY); curValue != RESULT { 215 | t.Errorf("Value wasn't incremented") 216 | } 217 | } 218 | 219 | func TestDecrement(t *testing.T) { 220 | KEY := "hits" 221 | VALUE := "1" 222 | MINUSVALUE := "1" 223 | RESULT := "0" 224 | 225 | new(100) 226 | s.Set(KEY, VALUE, 0) 227 | 228 | param := make(url.Values) 229 | param["key"] = []string{KEY} 230 | param["value"] = []string{MINUSVALUE} 231 | req, err := http.NewRequest("GET", "/decrement?"+param.Encode(), nil) 232 | if err != nil { 233 | t.Fatal(err) 234 | } 235 | 236 | rr := httptest.NewRecorder() 237 | handler := http.HandlerFunc(Decrement) 238 | handler.ServeHTTP(rr, req) 239 | 240 | if status := rr.Code; status != http.StatusNoContent { 241 | t.Errorf("Handler returned wrong status code: got %v want %v", 242 | status, http.StatusNoContent) 243 | } 244 | 245 | if curValue, _ := s.Get(KEY); curValue != RESULT { 246 | t.Errorf("Value wasn't decremented") 247 | } 248 | } 249 | 250 | func TestAppend(t *testing.T) { 251 | KEY := "name" 252 | VALUE := "And" 253 | APPENDVALUE := "y" 254 | RESULT := "Andy" 255 | 256 | new(100) 257 | s.Set(KEY, VALUE, 0) 258 | 259 | param := make(url.Values) 260 | param["key"] = []string{KEY} 261 | param["value"] = []string{APPENDVALUE} 262 | req, err := http.NewRequest("GET", "/append?"+param.Encode(), nil) 263 | if err != nil { 264 | t.Fatal(err) 265 | } 266 | 267 | rr := httptest.NewRecorder() 268 | handler := http.HandlerFunc(Append) 269 | handler.ServeHTTP(rr, req) 270 | 271 | if status := rr.Code; status != http.StatusNoContent { 272 | t.Errorf("Handler returned wrong status code: got %v want %v", 273 | status, http.StatusNoContent) 274 | } 275 | 276 | if curValue, _ := s.Get(KEY); curValue != RESULT { 277 | t.Errorf("Value wasn't appended") 278 | } 279 | } 280 | 281 | func TestPrepend(t *testing.T) { 282 | KEY := "name" 283 | VALUE := "ndy" 284 | PREPENDVALUE := "A" 285 | RESULT := "Andy" 286 | 287 | new(100) 288 | s.Set(KEY, VALUE, 0) 289 | 290 | param := make(url.Values) 291 | param["key"] = []string{KEY} 292 | param["value"] = []string{PREPENDVALUE} 293 | req, err := http.NewRequest("GET", "/prepend?"+param.Encode(), nil) 294 | if err != nil { 295 | t.Fatal(err) 296 | } 297 | 298 | rr := httptest.NewRecorder() 299 | handler := http.HandlerFunc(Prepend) 300 | handler.ServeHTTP(rr, req) 301 | 302 | if status := rr.Code; status != http.StatusNoContent { 303 | t.Errorf("Handler returned wrong status code: got %v want %v", 304 | status, http.StatusNoContent) 305 | } 306 | 307 | if curValue, _ := s.Get(KEY); curValue != RESULT { 308 | t.Errorf("Value wasn't prepended") 309 | } 310 | } 311 | 312 | func TestFlush(t *testing.T) { 313 | KEY := "name" 314 | VALUE := "Alice" 315 | 316 | new(100) 317 | s.Set(KEY, VALUE, 0) 318 | 319 | req, err := http.NewRequest("GET", "/flush", nil) 320 | if err != nil { 321 | t.Fatal(err) 322 | } 323 | 324 | rr := httptest.NewRecorder() 325 | handler := http.HandlerFunc(Flush) 326 | handler.ServeHTTP(rr, req) 327 | 328 | if status := rr.Code; status != http.StatusNoContent { 329 | t.Errorf("Handler returned wrong status code: got %v want %v", 330 | status, http.StatusNoContent) 331 | } 332 | 333 | if _, exist := s.Get(KEY); exist != false { 334 | t.Errorf("Cache wasn't flushed") 335 | } 336 | } 337 | 338 | func TestStats(t *testing.T) { 339 | KEY := "name" 340 | VALUE := "Mary" 341 | 342 | new(100) 343 | s.Set(KEY, VALUE, 0) 344 | 345 | req, err := http.NewRequest("GET", "/stats", nil) 346 | if err != nil { 347 | t.Fatal(err) 348 | } 349 | 350 | rr := httptest.NewRecorder() 351 | handler := http.HandlerFunc(Stats) 352 | handler.ServeHTTP(rr, req) 353 | 354 | if status := rr.Code; status != http.StatusOK { 355 | t.Errorf("Handler returned wrong status code: got %v want %v", 356 | status, http.StatusOK) 357 | } 358 | 359 | //TODO (healeycodes) 360 | // Could parse the stats and check but for now just sanity check the path 361 | } 362 | -------------------------------------------------------------------------------- /cache/cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "container/list" 5 | "fmt" 6 | "strconv" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | // Store contains an LRU Cache 12 | type Store struct { 13 | mutex *sync.Mutex 14 | store map[string]*list.Element 15 | ll *list.List 16 | max int // Zero for unlimited 17 | } 18 | 19 | // Node maps a value to a key 20 | type Node struct { 21 | key string 22 | value string 23 | expire int // Unix time 24 | } 25 | 26 | // Service returns an empty store 27 | func Service(max int) *Store { 28 | s := &Store{ 29 | mutex: &sync.Mutex{}, 30 | store: make(map[string]*list.Element), 31 | ll: list.New(), 32 | max: max, 33 | } 34 | return s 35 | } 36 | 37 | // Set a key 38 | func (s *Store) Set(key string, value string, expire int) { 39 | s.mutex.Lock() 40 | defer s.mutex.Unlock() 41 | s.set(key, value, expire) 42 | } 43 | 44 | // Internal set 45 | func (s *Store) set(key string, value string, expire int) { 46 | current, exist := s.store[key] 47 | if exist != true { 48 | s.store[key] = s.ll.PushFront(&Node{ 49 | key: key, 50 | value: value, 51 | expire: expire, 52 | }) 53 | if s.max != 0 && s.ll.Len() > s.max { 54 | s.delete(s.ll.Remove(s.ll.Back()).(*Node).key) 55 | } 56 | return 57 | } 58 | current.Value.(*Node).value = value 59 | current.Value.(*Node).expire = expire 60 | s.ll.MoveToFront(current) 61 | } 62 | 63 | // Get a key 64 | func (s *Store) Get(key string) (string, bool) { 65 | s.mutex.Lock() 66 | defer s.mutex.Unlock() 67 | current, exist := s.store[key] 68 | if exist { 69 | expire := int64(current.Value.(*Node).expire) 70 | if expire == 0 || expire > time.Now().Unix() { 71 | s.ll.MoveToFront(current) 72 | return current.Value.(*Node).value, true 73 | } 74 | } 75 | return "", false 76 | } 77 | 78 | // Delete an item 79 | func (s *Store) Delete(key string) { 80 | s.mutex.Lock() 81 | defer s.mutex.Unlock() 82 | s.delete(key) 83 | } 84 | 85 | // Internal delete 86 | func (s *Store) delete(key string) { 87 | current, exist := s.store[key] 88 | if exist != true { 89 | return 90 | } 91 | s.ll.Remove(current) 92 | delete(s.store, key) 93 | } 94 | 95 | // CheckAndSet a key. Sets only if the compare matches. Set the key if it doesn't exist 96 | func (s *Store) CheckAndSet(key string, value string, expire int, compare string) bool { 97 | s.mutex.Lock() 98 | defer s.mutex.Unlock() 99 | current, exist := s.store[key] 100 | if !exist || current.Value.(*Node).value == compare { 101 | s.set(key, value, expire) 102 | return true 103 | } 104 | return false 105 | } 106 | 107 | // Increment a key by an amount. Both value and amount should be integers. If doesn't exist, set to amount 108 | func (s *Store) Increment(key string, value string, expire int) error { 109 | s.mutex.Lock() 110 | defer s.mutex.Unlock() 111 | current, exist := s.store[key] 112 | if !exist { 113 | s.set(key, value, expire) 114 | } 115 | 116 | y, err := strconv.Atoi(value) 117 | if err != nil { 118 | return err 119 | } 120 | x, err := strconv.Atoi(current.Value.(*Node).value) 121 | if err != nil { 122 | return err 123 | } 124 | s.set(key, strconv.Itoa(x+y), expire) 125 | return nil 126 | } 127 | 128 | // Decrement a key by an amount. Both value and amount should be integers. If doesn't exist, set to amount 129 | func (s *Store) Decrement(key string, value string, expire int) error { 130 | s.mutex.Lock() 131 | defer s.mutex.Unlock() 132 | current, exist := s.store[key] 133 | if !exist { 134 | s.set(key, value, expire) 135 | } 136 | 137 | y, err := strconv.Atoi(value) 138 | if err != nil { 139 | return err 140 | } 141 | x, err := strconv.Atoi(current.Value.(*Node).value) 142 | if err != nil { 143 | return err 144 | } 145 | s.set(key, strconv.Itoa(x-y), expire) 146 | return nil 147 | } 148 | 149 | // Append to a key 150 | func (s *Store) Append(key string, value string, expire int) { 151 | s.mutex.Lock() 152 | defer s.mutex.Unlock() 153 | current, exist := s.store[key] 154 | if !exist { 155 | s.set(key, value, expire) 156 | return 157 | } 158 | s.set(key, current.Value.(*Node).value+value, expire) 159 | } 160 | 161 | // Prepend to a key 162 | func (s *Store) Prepend(key string, value string, expire int) { 163 | s.mutex.Lock() 164 | defer s.mutex.Unlock() 165 | current, exist := s.store[key] 166 | if !exist { 167 | s.set(key, value, expire) 168 | return 169 | } 170 | s.set(key, value+current.Value.(*Node).value, expire) 171 | } 172 | 173 | // Flush all keys 174 | func (s *Store) Flush() { 175 | s.mutex.Lock() 176 | defer s.mutex.Unlock() 177 | s.store = make(map[string]*list.Element) 178 | s.ll = list.New() 179 | } 180 | 181 | // Stats returns up-to-date information about the cache 182 | func (s *Store) Stats() string { 183 | s.mutex.Lock() 184 | defer s.mutex.Unlock() 185 | // TODO (healeycodes) 186 | // Use json package here 187 | return fmt.Sprintf(`{"keyCount": %v, "maxSize": %v}`, len(s.store), s.max) 188 | } 189 | -------------------------------------------------------------------------------- /cache/cache_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import "testing" 4 | 5 | func TestLRUSize(t *testing.T) { 6 | KEYONE := "name" 7 | KEYTWO := "place" 8 | VALUEONE := "Andrew" 9 | VALUETWO := "Moon" 10 | 11 | // Test a cache with keys limited to one 12 | s := Service(1) 13 | 14 | s.Set(KEYONE, VALUEONE, 0) 15 | 16 | if curValue, ok := s.Get(KEYONE); ok != true || curValue != VALUEONE { 17 | t.Errorf("Problem setting and getting key") 18 | } 19 | 20 | s.Set(KEYTWO, VALUETWO, 0) 21 | 22 | if _, ok := s.Get(KEYONE); ok == true { 23 | t.Errorf("First key wasn't removed from cache as it become oversized") 24 | } 25 | } 26 | 27 | func TestLRUExpiring(t *testing.T) { 28 | KEYONE := "name" 29 | VALUEONE := "Andrew" 30 | EXPIRE := 1 31 | 32 | // Test a cache with an expired key 33 | s := Service(1) 34 | 35 | s.Set(KEYONE, VALUEONE, EXPIRE) 36 | 37 | if _, ok := s.Get(KEYONE); ok == true { 38 | t.Errorf("Key should have expired") 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module healeycodes/in-memory-cache-over-http 2 | 3 | go 1.13 4 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | api "healeycodes/in-memory-cache-over-http/api" 5 | ) 6 | 7 | func main() { 8 | api.Listen() 9 | } 10 | --------------------------------------------------------------------------------