Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
WIP implementation of a memory limit. This will likely be superseded
by Go's incoming soft memory limit feature (coming August?), but it's
interesting to explore nonetheless.

Each time we receive a PUT request, check the used memory. To calculate
used memory, we use runtime.ReadMemStats. I was concerned that it would
have a large performance cost, because it stops the world on every
invocation, but it turns out that it has previously been optimised.
Return a 500 if this value has exceeded the current max memory. We
use TotalAlloc do determine used memory, because this seemed to be
closest to the container memory usage reported by Docker. This is broken
regardless, because the value does not decrease as we delete keys
(possibly because the store map does not shrink).

If we can work out a constant overhead for the map data structure, we
might be able to compute memory usage based on the size of keys and
values. I think it will be difficult to do this reliably, though. Given
that a new language feature will likely remove the need for this work,
a simple interim solution might be to implement a max number of objects
limit, which provides some value in situations where the user can
predict the size of keys and values.

TODO:

* Make the memory limit configurable by way of an environment variable
* Push the limit checking code down to the put handler

golang/go#48409
golang/go@4a7cf96
patrickmn/go-cache#5
https://github.com/vitessio/vitess/blob/main/go/cache/lru_cache.go
golang/go#20135
https://redis.io/docs/getting-started/faq/#what-happens-if-redis-runs-out-of-memory
https://redis.io/docs/manual/eviction/
  • Loading branch information
tjvc committed May 15, 2022
1 parent d296faa commit d5723a7
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 6 deletions.
31 changes: 27 additions & 4 deletions cmd/gauche/main.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package main

import (
"fmt"
"net/http"
"runtime"

"github.com/tjvc/gauche/internal/handler"
"github.com/tjvc/gauche/internal/logging"
Expand All @@ -10,8 +12,9 @@ import (
)

type application struct {
store *store.Store
logger logging.Logger
store *store.Store
logger logging.Logger
maxMemMB int
}

func mainHandler(application application) http.Handler {
Expand All @@ -26,12 +29,30 @@ func mainHandler(application application) http.Handler {
return
}

application.store.Set("dummy", []byte("123"))

var m runtime.MemStats
runtime.ReadMemStats(&m)
usedMemMB := float64(m.TotalAlloc) / 1024 / 1024
maxMemMB := float64(application.maxMemMB)
fmt.Printf("usedMemMB: %f\n", usedMemMB)
fmt.Printf("maxMemMB: %f\n", maxMemMB)
fmt.Printf("TotalAlloc: %d\n", m.TotalAlloc)
fmt.Printf("Alloc: %d\n", m.Alloc)
fmt.Printf("Store size: %d\n", application.store.Size)
fmt.Printf("Ratio: %f\n", float64(m.Alloc)/float64(application.store.Size))

key := r.URL.Path[1:]

switch r.Method {
case http.MethodGet:
handler.Get(w, key, application.store)
case http.MethodPut:
if usedMemMB > maxMemMB {
w.WriteHeader(http.StatusInternalServerError)
return
}

handler.Put(w, key, r, application.store)
case http.MethodDelete:
handler.Delete(w, key, application.store)
Expand All @@ -48,10 +69,12 @@ func mainHandler(application application) http.Handler {
func main() {
store := store.New()
logger := logging.JSONLogger{}
maxMemMB := 1024

application := application{
store: &store,
logger: logger,
store: &store,
logger: logger,
maxMemMB: maxMemMB,
}

http.ListenAndServe(":8080", mainHandler(application))
Expand Down
25 changes: 23 additions & 2 deletions cmd/gauche/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,26 @@ func TestPutNilValue(t *testing.T) {
}
}

func TestMemLimit(t *testing.T) {
logger := nullLogger{}
store := store.New()
application := application{
store: &store,
logger: logger,
maxMemMB: 0,
}
server := httptest.NewServer(mainHandler(application))
defer server.Close()
url := fmt.Sprintf("%s/key", server.URL)
req, _ := http.NewRequest("PUT", url, strings.NewReader("value"))

response, _ := http.DefaultClient.Do(req)

if response.StatusCode != 500 {
t.Errorf("got %d, want %d", response.StatusCode, 500)
}
}

func buildServer(store *store.Store) *httptest.Server {
application := buildApplication(store)
server := httptest.NewServer(mainHandler(application))
Expand All @@ -164,8 +184,9 @@ func buildApplication(store *store.Store) application {
logger := nullLogger{}

return application{
store: store,
logger: logger,
store: store,
logger: logger,
maxMemMB: 1024,
}
}

Expand Down
3 changes: 3 additions & 0 deletions internal/store/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
type Store struct {
sync.RWMutex
store map[string][]byte
Size int
}

func New() Store {
Expand All @@ -27,6 +28,8 @@ func (store *Store) Set(key string, value []byte) {
store.Lock()
defer store.Unlock()
store.store[key] = value
store.Size += len(key)
store.Size += len(value)
}

func (store *Store) Delete(key string) {
Expand Down

0 comments on commit d5723a7

Please sign in to comment.