Skip to content

Commit d8d4036

Browse files
authored
GT-575 Transparent compression of requests and responses (#598)
1 parent f2af292 commit d8d4036

File tree

12 files changed

+371
-14
lines changed

12 files changed

+371
-14
lines changed

.circleci/config.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ jobs:
5555
test-to-run:
5656
type: string
5757
default: "run-tests-single"
58+
enable-extra-db-features:
59+
type: boolean
60+
default: false
5861
steps:
5962
- checkout
6063
- run:
@@ -71,6 +74,7 @@ jobs:
7174
GOIMAGE: << pipeline.parameters.goImage >>
7275
ALPINE_IMAGE: << pipeline.parameters.alpineImage >>
7376
STARTER: << pipeline.parameters.starterImage >>
77+
ENABLE_DATABASE_EXTRA_FEATURES: << parameters.enable-extra-db-features >>
7478
TEST_DISALLOW_UNKNOWN_FIELDS: false
7579
VERBOSE: 1
7680

@@ -123,6 +127,12 @@ workflows:
123127
requires:
124128
- download-demo-data
125129
test-to-run: run-v2-tests-cluster
130+
- run-integration-tests:
131+
name: Test V2 cluster - DB extra features (compression)
132+
requires:
133+
- download-demo-data
134+
test-to-run: run-v2-tests-cluster
135+
enable-extra-db-features: true
126136

127137
- run-integration-tests:
128138
name: Test V1 single

Makefile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,7 @@ __test_go_test:
414414
-e TEST_ENABLE_SHUTDOWN=$(TEST_ENABLE_SHUTDOWN) \
415415
-e TEST_REQUEST_LOG=$(TEST_REQUEST_LOG) \
416416
-e TEST_DISALLOW_UNKNOWN_FIELDS=$(TEST_DISALLOW_UNKNOWN_FIELDS) \
417+
-e ENABLE_DATABASE_EXTRA_FEATURES=$(ENABLE_DATABASE_EXTRA_FEATURES) \
417418
-e GODEBUG=tls13=1 \
418419
-e CGO_ENABLED=$(CGO_ENABLED) \
419420
-w /usr/code/ \
@@ -441,6 +442,7 @@ __test_v2_go_test:
441442
-e TEST_BACKUP_REMOTE_CONFIG='$(TEST_BACKUP_REMOTE_CONFIG)' \
442443
-e TEST_DEBUG='$(TEST_DEBUG)' \
443444
-e TEST_ENABLE_SHUTDOWN=$(TEST_ENABLE_SHUTDOWN) \
445+
-e ENABLE_DATABASE_EXTRA_FEATURES=$(ENABLE_DATABASE_EXTRA_FEATURES) \
444446
-e GODEBUG=tls13=1 \
445447
-e CGO_ENABLED=$(CGO_ENABLED) \
446448
-w /usr/code/v2/ \
@@ -475,7 +477,9 @@ ifdef JWTSECRET
475477
echo "$JWTSECRET" > "${JWTSECRETFILE}"
476478
endif
477479
@-docker rm -f -v $(TESTCONTAINER) &> /dev/null
478-
@TESTCONTAINER=$(TESTCONTAINER) ARANGODB=$(ARANGODB) ALPINE_IMAGE=$(ALPINE_IMAGE) ENABLE_BACKUP=$(ENABLE_BACKUP) ARANGO_LICENSE_KEY=$(ARANGO_LICENSE_KEY) STARTER=$(STARTER) STARTERMODE=$(TEST_MODE) TMPDIR="${TMPDIR}" DEBUG_PORT=$(DEBUG_PORT) $(CLUSTERENV) "${ROOTDIR}/test/cluster.sh" start
480+
@TESTCONTAINER=$(TESTCONTAINER) ARANGODB=$(ARANGODB) ALPINE_IMAGE=$(ALPINE_IMAGE) ENABLE_BACKUP=$(ENABLE_BACKUP) \
481+
ARANGO_LICENSE_KEY=$(ARANGO_LICENSE_KEY) STARTER=$(STARTER) STARTERMODE=$(TEST_MODE) TMPDIR="${TMPDIR}" \
482+
ENABLE_DATABASE_EXTRA_FEATURES=$(ENABLE_DATABASE_EXTRA_FEATURES) DEBUG_PORT=$(DEBUG_PORT) $(CLUSTERENV) "${ROOTDIR}/test/cluster.sh" start
479483
endif
480484

481485
__test_cleanup:

test/cluster.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ if [ "$CMD" == "start" ]; then
4848
if [ -n "$ENABLE_BACKUP" ]; then
4949
STARTERARGS="$STARTERARGS --all.backup.api-enabled=true"
5050
fi
51-
if [ -n "$ENABLE_DATABASE_EXTENDED_NAMES" ]; then
52-
STARTERARGS="$STARTERARGS --all.database.extended-names-databases=true"
51+
if [ -n "$ENABLE_DATABASE_EXTRA_FEATURES" ]; then
52+
STARTERARGS="$STARTERARGS --all.database.extended-names-databases=true --args.all.http.compress-response-threshold=1 --args.all.http.handle-content-encoding-for-unauthenticated-requests=true"
5353
fi
5454
if [[ "$OSTYPE" == "darwin"* ]]; then
5555
DOCKERPLATFORMARG="--platform linux/x86_64"

v2/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
- Backup API support
2020
- Admin Cluster API support
2121
- Set Licence API support
22+
- Transparent compression of requests and responses (ArangoDBConfiguration.Compression)
2223

2324

2425
## [2.0.3](https://github.com/arangodb/go-driver/tree/v2.0.3) (2023-10-31)

v2/connection/connection.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,12 @@ package connection
2323
import (
2424
"context"
2525
"io"
26+
"net/http"
2627
)
2728

29+
type EncodingCodec interface {
30+
}
31+
2832
type Wrapper func(c Connection) Connection
2933

3034
type Factory func() (Connection, error)
@@ -40,8 +44,38 @@ type ArangoDBConfiguration struct {
4044

4145
// DriverFlags configure additional flags for the `x-arango-driver` header
4246
DriverFlags []string
47+
48+
// Compression is used to enable compression between client and server
49+
Compression *CompressionConfig
50+
}
51+
52+
// CompressionConfig is used to enable compression for the connection
53+
type CompressionConfig struct {
54+
// CompressionConfig is used to enable compression for the requests
55+
CompressionType CompressionType
56+
57+
// ResponseCompressionEnabled is used to enable compression for the responses (requires server side adjustments)
58+
ResponseCompressionEnabled bool
59+
60+
// RequestCompressionEnabled is used to enable compression for the requests
61+
RequestCompressionEnabled bool
62+
63+
// RequestCompressionLevel - Sets the compression level between -1 and 9
64+
// Default: 0 (NoCompression). For Reference see: https://pkg.go.dev/compress/flate#pkg-constants
65+
RequestCompressionLevel int
4366
}
4467

68+
type CompressionType string
69+
70+
const (
71+
72+
// RequestCompressionTypeGzip is used to enable gzip compression
73+
RequestCompressionTypeGzip CompressionType = "gzip"
74+
75+
// RequestCompressionTypeDeflate is used to enable deflate compression
76+
RequestCompressionTypeDeflate CompressionType = "deflate"
77+
)
78+
4579
type Connection interface {
4680
// NewRequest initializes Request object
4781
NewRequest(method string, urls ...string) (Request, error)
@@ -111,4 +145,6 @@ type Response interface {
111145
// Header gets the first value associated with the given key.
112146
// If there are no values associated with the key, Get returns "".
113147
Header(name string) string
148+
149+
RawResponse() *http.Response
114150
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
//
2+
// DISCLAIMER
3+
//
4+
// Copyright 2024 ArangoDB GmbH, Cologne, Germany
5+
//
6+
// Licensed under the Apache License, Version 2.0 (the "License");
7+
// you may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
//
18+
// Copyright holder is ArangoDB GmbH, Cologne, Germany
19+
//
20+
21+
package connection
22+
23+
import (
24+
"compress/zlib"
25+
"fmt"
26+
"io"
27+
28+
"github.com/arangodb/go-driver/v2/log"
29+
)
30+
31+
type Compression interface {
32+
ApplyRequestHeaders(r Request)
33+
ApplyRequestCompression(r *httpRequest, rootWriter io.Writer) (io.WriteCloser, error)
34+
}
35+
36+
func newCompression(config *CompressionConfig) Compression {
37+
if config == nil {
38+
return noCompression{}
39+
} else if config.CompressionType == "gzip" {
40+
return gzipCompression{config: config}
41+
} else if config.CompressionType == "deflate" {
42+
return deflateCompression{config: config}
43+
} else {
44+
log.Error(fmt.Errorf("unknown compression type: %s", config.CompressionType), "")
45+
return noCompression{config: config}
46+
}
47+
}
48+
49+
type gzipCompression struct {
50+
config *CompressionConfig
51+
}
52+
53+
func (g gzipCompression) ApplyRequestHeaders(r Request) {
54+
if g.config != nil && g.config.ResponseCompressionEnabled {
55+
if g.config.CompressionType == "gzip" {
56+
r.AddHeader("Accept-Encoding", "gzip")
57+
}
58+
}
59+
}
60+
61+
func (g gzipCompression) ApplyRequestCompression(r *httpRequest, rootWriter io.Writer) (io.WriteCloser, error) {
62+
config := g.config
63+
64+
if config != nil && config.RequestCompressionEnabled {
65+
if config.CompressionType == "deflate" {
66+
r.headers["Content-Encoding"] = "deflate"
67+
68+
zlibWriter, err := zlib.NewWriterLevel(rootWriter, config.RequestCompressionLevel)
69+
if err != nil {
70+
log.Errorf(err, "error creating zlib writer")
71+
return nil, err
72+
}
73+
74+
return zlibWriter, nil
75+
}
76+
}
77+
78+
return nil, nil
79+
}
80+
81+
type deflateCompression struct {
82+
config *CompressionConfig
83+
}
84+
85+
func (g deflateCompression) ApplyRequestHeaders(r Request) {
86+
if g.config != nil && g.config.ResponseCompressionEnabled {
87+
if g.config.CompressionType == "deflate" {
88+
r.AddHeader("Accept-Encoding", "deflate")
89+
}
90+
}
91+
}
92+
93+
func (g deflateCompression) ApplyRequestCompression(r *httpRequest, rootWriter io.Writer) (io.WriteCloser, error) {
94+
config := g.config
95+
96+
if config != nil && config.RequestCompressionEnabled {
97+
if config.CompressionType == "deflate" {
98+
r.headers["Content-Encoding"] = "deflate"
99+
100+
zlibWriter, err := zlib.NewWriterLevel(rootWriter, config.RequestCompressionLevel)
101+
if err != nil {
102+
log.Errorf(err, "error creating zlib writer")
103+
return nil, err
104+
}
105+
106+
return zlibWriter, nil
107+
}
108+
}
109+
110+
return nil, nil
111+
}
112+
113+
type noCompression struct {
114+
config *CompressionConfig
115+
}
116+
117+
func (g noCompression) ApplyRequestHeaders(r Request) {
118+
}
119+
120+
func (g noCompression) ApplyRequestCompression(r *httpRequest, rootWriter io.Writer) (io.WriteCloser, error) {
121+
return nil, nil
122+
}

v2/connection/connection_http_internal.go

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ package connection
2222

2323
import (
2424
"bytes"
25+
"compress/gzip"
26+
"compress/zlib"
2527
"context"
2628
"crypto/tls"
2729
"io"
@@ -248,7 +250,7 @@ func (j *httpConnection) stream(ctx context.Context, req *httpRequest) (*httpRes
248250
ctx = context.Background()
249251
}
250252

251-
reader := j.bodyReadFunc(j.Decoder(j.contentType), req.body, j.streamSender)
253+
reader := j.bodyReadFunc(j.Decoder(j.contentType), req, j.streamSender)
252254
r, err := req.asRequest(ctx, reader)
253255
if err != nil {
254256
return nil, nil, errors.WithStack(err)
@@ -263,9 +265,19 @@ func (j *httpConnection) stream(ctx context.Context, req *httpRequest) (*httpRes
263265
log.Debugf("(%s) Response received: %d", id, resp.StatusCode)
264266

265267
if b := resp.Body; b != nil {
266-
var body = resp.Body
268+
var resultBody io.ReadCloser
269+
270+
respEncoding := resp.Header.Get("Content-Encoding")
271+
switch respEncoding {
272+
case "gzip":
273+
resultBody, err = gzip.NewReader(resp.Body)
274+
case "deflate":
275+
resultBody, err = zlib.NewReader(resp.Body)
276+
default:
277+
resultBody = resp.Body
278+
}
267279

268-
return &httpResponse{response: resp, request: req}, body, nil
280+
return &httpResponse{response: resp, request: req}, resultBody, nil
269281

270282
}
271283

@@ -289,8 +301,8 @@ func getDecoderByContentType(contentType string) Decoder {
289301

290302
type bodyReadFactory func() (io.Reader, error)
291303

292-
func (j *httpConnection) bodyReadFunc(decoder Decoder, obj interface{}, stream bool) bodyReadFactory {
293-
if obj == nil {
304+
func (j *httpConnection) bodyReadFunc(decoder Decoder, req *httpRequest, stream bool) bodyReadFactory {
305+
if req.body == nil {
294306
return func() (io.Reader, error) {
295307
return nil, nil
296308
}
@@ -299,21 +311,64 @@ func (j *httpConnection) bodyReadFunc(decoder Decoder, obj interface{}, stream b
299311
if !stream {
300312
return func() (io.Reader, error) {
301313
b := bytes.NewBuffer([]byte{})
302-
if err := decoder.Encode(b, obj); err != nil {
303-
log.Errorf(err, "error encoding body - OBJ: %v", obj)
314+
compressedWriter, err := newCompression(j.config.Compression).ApplyRequestCompression(req, b)
315+
if err != nil {
316+
log.Errorf(err, "error applying compression")
304317
return nil, err
305318
}
306319

307-
return b, nil
320+
if compressedWriter != nil {
321+
defer func(compressedWriter io.WriteCloser) {
322+
errCompression := compressedWriter.Close()
323+
if errCompression != nil {
324+
log.Error(errCompression, "error closing compressed writer")
325+
if err == nil {
326+
err = errCompression
327+
}
328+
}
329+
}(compressedWriter)
330+
331+
err = decoder.Encode(compressedWriter, req.body)
332+
} else {
333+
err = decoder.Encode(b, req.body)
334+
}
335+
336+
if err != nil {
337+
log.Errorf(err, "error encoding body - OBJ: %v", req.body)
338+
return nil, err
339+
}
340+
return b, err
308341
}
309342
} else {
310343
return func() (io.Reader, error) {
311344
reader, writer := io.Pipe()
345+
346+
compressedWriter, err := newCompression(j.config.Compression).ApplyRequestCompression(req, writer)
347+
if err != nil {
348+
log.Errorf(err, "error applying compression")
349+
return nil, err
350+
}
351+
312352
go func() {
313353
defer writer.Close()
314354

315-
if err := decoder.Encode(writer, obj); err != nil {
316-
log.Errorf(err, "error encoding body (stream) - OBJ: %v", obj)
355+
var encErr error
356+
if compressedWriter != nil {
357+
defer func(compressedWriter io.WriteCloser) {
358+
errCompression := compressedWriter.Close()
359+
if errCompression != nil {
360+
log.Errorf(errCompression, "error closing compressed writer - stream")
361+
writer.CloseWithError(err)
362+
}
363+
}(compressedWriter)
364+
365+
encErr = decoder.Encode(compressedWriter, req.body)
366+
} else {
367+
encErr = decoder.Encode(writer, req.body)
368+
}
369+
370+
if encErr != nil {
371+
log.Errorf(err, "error encoding body stream - OBJ: %v", req.body)
317372
writer.CloseWithError(err)
318373
}
319374
}()

v2/connection/connection_http_response.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,7 @@ func (j *httpResponse) Header(name string) string {
5858
}
5959
return j.response.Header.Get(name)
6060
}
61+
62+
func (j *httpResponse) RawResponse() *http.Response {
63+
return j.response
64+
}

v2/connection/modifiers.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ func applyArangoDBConfiguration(config ArangoDBConfiguration, ctx context.Contex
104104
}
105105
}
106106

107+
newCompression(config.Compression).ApplyRequestHeaders(r)
108+
107109
return nil
108110
}
109111
}

0 commit comments

Comments
 (0)