├── .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/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 |
--------------------------------------------------------------------------------