Skip to content

Commit d5723a7

Browse files
committed
WIP
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/
1 parent d296faa commit d5723a7

File tree

3 files changed

+53
-6
lines changed

3 files changed

+53
-6
lines changed

cmd/gauche/main.go

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package main
22

33
import (
4+
"fmt"
45
"net/http"
6+
"runtime"
57

68
"github.com/tjvc/gauche/internal/handler"
79
"github.com/tjvc/gauche/internal/logging"
@@ -10,8 +12,9 @@ import (
1012
)
1113

1214
type application struct {
13-
store *store.Store
14-
logger logging.Logger
15+
store *store.Store
16+
logger logging.Logger
17+
maxMemMB int
1518
}
1619

1720
func mainHandler(application application) http.Handler {
@@ -26,12 +29,30 @@ func mainHandler(application application) http.Handler {
2629
return
2730
}
2831

32+
application.store.Set("dummy", []byte("123"))
33+
34+
var m runtime.MemStats
35+
runtime.ReadMemStats(&m)
36+
usedMemMB := float64(m.TotalAlloc) / 1024 / 1024
37+
maxMemMB := float64(application.maxMemMB)
38+
fmt.Printf("usedMemMB: %f\n", usedMemMB)
39+
fmt.Printf("maxMemMB: %f\n", maxMemMB)
40+
fmt.Printf("TotalAlloc: %d\n", m.TotalAlloc)
41+
fmt.Printf("Alloc: %d\n", m.Alloc)
42+
fmt.Printf("Store size: %d\n", application.store.Size)
43+
fmt.Printf("Ratio: %f\n", float64(m.Alloc)/float64(application.store.Size))
44+
2945
key := r.URL.Path[1:]
3046

3147
switch r.Method {
3248
case http.MethodGet:
3349
handler.Get(w, key, application.store)
3450
case http.MethodPut:
51+
if usedMemMB > maxMemMB {
52+
w.WriteHeader(http.StatusInternalServerError)
53+
return
54+
}
55+
3556
handler.Put(w, key, r, application.store)
3657
case http.MethodDelete:
3758
handler.Delete(w, key, application.store)
@@ -48,10 +69,12 @@ func mainHandler(application application) http.Handler {
4869
func main() {
4970
store := store.New()
5071
logger := logging.JSONLogger{}
72+
maxMemMB := 1024
5173

5274
application := application{
53-
store: &store,
54-
logger: logger,
75+
store: &store,
76+
logger: logger,
77+
maxMemMB: maxMemMB,
5578
}
5679

5780
http.ListenAndServe(":8080", mainHandler(application))

cmd/gauche/main_test.go

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,26 @@ func TestPutNilValue(t *testing.T) {
154154
}
155155
}
156156

157+
func TestMemLimit(t *testing.T) {
158+
logger := nullLogger{}
159+
store := store.New()
160+
application := application{
161+
store: &store,
162+
logger: logger,
163+
maxMemMB: 0,
164+
}
165+
server := httptest.NewServer(mainHandler(application))
166+
defer server.Close()
167+
url := fmt.Sprintf("%s/key", server.URL)
168+
req, _ := http.NewRequest("PUT", url, strings.NewReader("value"))
169+
170+
response, _ := http.DefaultClient.Do(req)
171+
172+
if response.StatusCode != 500 {
173+
t.Errorf("got %d, want %d", response.StatusCode, 500)
174+
}
175+
}
176+
157177
func buildServer(store *store.Store) *httptest.Server {
158178
application := buildApplication(store)
159179
server := httptest.NewServer(mainHandler(application))
@@ -164,8 +184,9 @@ func buildApplication(store *store.Store) application {
164184
logger := nullLogger{}
165185

166186
return application{
167-
store: store,
168-
logger: logger,
187+
store: store,
188+
logger: logger,
189+
maxMemMB: 1024,
169190
}
170191
}
171192

internal/store/store.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
type Store struct {
99
sync.RWMutex
1010
store map[string][]byte
11+
Size int
1112
}
1213

1314
func New() Store {
@@ -27,6 +28,8 @@ func (store *Store) Set(key string, value []byte) {
2728
store.Lock()
2829
defer store.Unlock()
2930
store.store[key] = value
31+
store.Size += len(key)
32+
store.Size += len(value)
3033
}
3134

3235
func (store *Store) Delete(key string) {

0 commit comments

Comments
 (0)