Skip to content

Commit 3ece4f4

Browse files
authored
fix: Improve compression performance via new provider (#532)
1 parent 4c212fd commit 3ece4f4

File tree

4 files changed

+144
-9
lines changed

4 files changed

+144
-9
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ require (
6060

6161
require (
6262
github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9
63+
github.com/klauspost/compress v1.18.0
6364
github.com/launchdarkly/api-client-go/v13 v13.0.1-0.20230420175109-f5469391a13e
6465
)
6566

@@ -84,7 +85,6 @@ require (
8485
github.com/google/s2a-go v0.1.7 // indirect
8586
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
8687
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
87-
github.com/gorilla/handlers v1.5.2 // indirect
8888
github.com/hashicorp/errwrap v1.1.0 // indirect
8989
github.com/hashicorp/go-multierror v1.1.1 // indirect
9090
github.com/hashicorp/go-rootcerts v1.0.2 // indirect

go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,8 +234,6 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+
234234
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
235235
github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=
236236
github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU=
237-
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
238-
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
239237
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
240238
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
241239
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA=

relay/relay_routes.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import (
1414
"github.com/launchdarkly/go-sdk-common/v3/ldlog"
1515
ldevents "github.com/launchdarkly/go-sdk-events/v3"
1616

17-
h "github.com/gorilla/handlers"
1817
"github.com/gorilla/mux"
18+
h "github.com/klauspost/compress/gzhttp"
1919
)
2020

2121
const (
@@ -36,7 +36,9 @@ func (r *Relay) makeRouter() *mux.Router {
3636
router.Use(logging.RequestLoggerMiddleware(r.loggers))
3737
}
3838
if r.config.HTTP.EnableCompression {
39-
router.Use(h.CompressHandler)
39+
router.Use(func(next http.Handler) http.Handler {
40+
return h.GzipHandler(next)
41+
})
4042
}
4143
router.Handle("/status", statusHandler(r)).Methods("GET")
4244

relay/relay_test.go

Lines changed: 139 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,23 @@ package relay
33
import (
44
"bytes"
55
"compress/gzip"
6+
"fmt"
67
"io"
78
"net/http"
89
"net/http/httptest"
10+
"runtime"
11+
"runtime/debug"
12+
"sync"
913
"testing"
14+
"time"
1015

1116
c "github.com/launchdarkly/ld-relay/v8/config"
1217

1318
"github.com/launchdarkly/go-configtypes"
1419
"github.com/launchdarkly/go-sdk-common/v3/ldlog"
1520
"github.com/launchdarkly/go-test-helpers/v3/httphelpers"
1621

22+
"github.com/klauspost/compress/gzhttp"
1723
"github.com/stretchr/testify/assert"
1824
"github.com/stretchr/testify/require"
1925
)
@@ -185,9 +191,10 @@ func TestCompressionIsAppliedWhenEnabled(t *testing.T) {
185191
}
186192

187193
withStartedRelay(t, configWithCompression, func(p relayTestParams) {
188-
// Create a request to the status endpoint
189-
req, _ := http.NewRequest("GET", "/status", nil)
194+
// Create a request to the flags endpoint which returns more data
195+
req, _ := http.NewRequest("GET", "/sdk/flags", nil)
190196
req.Header.Set("Accept-Encoding", "gzip")
197+
req.Header.Set("Authorization", "test-key")
191198

192199
w := httptest.NewRecorder()
193200
p.relay.ServeHTTP(w, req)
@@ -208,6 +215,9 @@ func TestCompressionIsAppliedWhenEnabled(t *testing.T) {
208215
decompressed, err := io.ReadAll(reader)
209216
assert.NoError(t, err, "Should be able to read decompressed content")
210217
assert.Greater(t, len(decompressed), 0, "Decompressed content should not be empty")
218+
219+
// Verify the decompressed content is substantial enough to test compression
220+
assert.Greater(t, len(decompressed), gzhttp.DefaultMinSize, fmt.Sprintf("Decompressed content should be larger than %d bytes to properly test compression", gzhttp.DefaultMinSize))
211221
})
212222
}
213223

@@ -225,9 +235,10 @@ func TestCompressionIsNotAppliedWhenDisabled(t *testing.T) {
225235
}
226236

227237
withStartedRelay(t, configWithoutCompression, func(p relayTestParams) {
228-
// Create a request to the status endpoint
229-
req, _ := http.NewRequest("GET", "/status", nil)
238+
// Create a request to the flags endpoint which returns more data
239+
req, _ := http.NewRequest("GET", "/sdk/flags", nil)
230240
req.Header.Set("Accept-Encoding", "gzip")
241+
req.Header.Set("Authorization", "test-key")
231242

232243
w := httptest.NewRecorder()
233244
p.relay.ServeHTTP(w, req)
@@ -240,8 +251,132 @@ func TestCompressionIsNotAppliedWhenDisabled(t *testing.T) {
240251
body := w.Body.Bytes()
241252
assert.Greater(t, len(body), 0, "Response body should not be empty")
242253

254+
// Verify the uncompressed content is substantial enough
255+
assert.Greater(t, len(body), gzhttp.DefaultMinSize, fmt.Sprintf("Uncompressed content should be larger than %d bytes", gzhttp.DefaultMinSize))
256+
257+
t.Logf("Body size: %d bytes", len(body))
243258
// Try to decompress the body - this should fail since it's not compressed
244259
_, err := gzip.NewReader(io.NopCloser(bytes.NewReader(body)))
245260
assert.Error(t, err, "Response should not be gzip content when compression is disabled")
246261
})
247262
}
263+
264+
func TestLoadCompression(t *testing.T) {
265+
// Set minimal memory limit for the test
266+
originalLimit := debug.SetMemoryLimit(10 * 1024 * 1024) // 10MB limit
267+
defer debug.SetMemoryLimit(originalLimit)
268+
269+
// Force garbage collection before starting
270+
runtime.GC()
271+
272+
// Test with compression enabled
273+
configWithCompression := c.Config{
274+
HTTP: c.HTTPConfig{
275+
EnableCompression: true,
276+
},
277+
Environment: map[string]*c.EnvConfig{
278+
"test": {
279+
SDKKey: "test-key",
280+
},
281+
},
282+
}
283+
284+
withStartedRelay(t, configWithCompression, func(p relayTestParams) {
285+
var wg sync.WaitGroup
286+
var mu sync.Mutex
287+
var requestCount int
288+
var lastError error
289+
var failed bool
290+
var maxAlloc uint64
291+
292+
// Start with a reasonable number of concurrent requests
293+
concurrency := 100
294+
maxRequests := 1000
295+
296+
for i := 0; i < concurrency; i++ {
297+
wg.Add(1)
298+
go func() {
299+
defer wg.Done()
300+
301+
for {
302+
mu.Lock()
303+
if failed || requestCount >= maxRequests {
304+
mu.Unlock()
305+
return
306+
}
307+
currentCount := requestCount
308+
requestCount++
309+
mu.Unlock()
310+
311+
// Create a request to the flags endpoint
312+
req, err := http.NewRequest("GET", "/sdk/flags", nil)
313+
if err != nil {
314+
mu.Lock()
315+
lastError = err
316+
failed = true
317+
mu.Unlock()
318+
return
319+
}
320+
321+
req.Header.Set("Accept-Encoding", "gzip")
322+
req.Header.Set("Authorization", "test-key")
323+
324+
w := httptest.NewRecorder()
325+
p.relay.ServeHTTP(w, req)
326+
327+
// Check if the request succeeded
328+
if w.Result().StatusCode != http.StatusOK {
329+
mu.Lock()
330+
lastError = fmt.Errorf("request failed with status: %d", w.Result().StatusCode)
331+
failed = true
332+
mu.Unlock()
333+
return
334+
}
335+
336+
// Verify compression is working
337+
if w.Header().Get("Content-Encoding") != "gzip" {
338+
mu.Lock()
339+
lastError = fmt.Errorf("compression not applied at request %d", currentCount)
340+
failed = true
341+
mu.Unlock()
342+
return
343+
}
344+
345+
// Check memory usage
346+
var m runtime.MemStats
347+
runtime.ReadMemStats(&m)
348+
349+
// Track maximum memory usage
350+
mu.Lock()
351+
if m.Alloc > maxAlloc {
352+
maxAlloc = m.Alloc
353+
}
354+
mu.Unlock()
355+
356+
// Small delay to prevent overwhelming the system
357+
time.Sleep(1 * time.Millisecond)
358+
}
359+
}()
360+
}
361+
362+
wg.Wait()
363+
364+
// Report results
365+
t.Logf("Load test completed: %d requests processed", requestCount)
366+
367+
if failed {
368+
t.Fatalf("Test failed: %v", lastError)
369+
}
370+
371+
t.Logf("Test completed successfully with %d requests", requestCount)
372+
assert.Greater(t, requestCount, 0, "Should have processed requests successfully")
373+
374+
// Final memory stats
375+
var m runtime.MemStats
376+
runtime.ReadMemStats(&m)
377+
t.Logf("Final memory usage: Alloc=%d MB, TotalAlloc=%d MB, NumGC=%d",
378+
m.Alloc/1024/1024, m.TotalAlloc/1024/1024, m.NumGC)
379+
t.Logf("Peak memory usage: MaxAlloc=%d MB",
380+
maxAlloc/1024/1024)
381+
})
382+
}

0 commit comments

Comments
 (0)