Skip to content

Commit dd361de

Browse files
authored
[exporter/awskinesisexporter] fix in compressor crashing under heady load due non-safe thread execution (#32589)
Fixing a bug that made the execution panic when the load was high enough, specially if the payloads were not very tiny. **Testing:** Executed this with and without the fix locally using heavy load, then ran it in cloud servers using heavier load and a variety of payloads.
1 parent 9501bb6 commit dd361de

File tree

6 files changed

+177
-98
lines changed

6 files changed

+177
-98
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Use this changelog template to create an entry for release notes.
2+
3+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
4+
change_type: 'bug_fix'
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
7+
component: awskinesisexporter
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: the compressor was crashing under high load due it not being thread safe.
11+
12+
# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
13+
issues: [32589]
14+
15+
# (Optional) One or more lines of additional information to render under the primary note.
16+
# These lines will be padded with 2 spaces and then inserted directly into the document.
17+
# Use pipe (|) for multiline entries.
18+
subtext: removed compressor abstraction and each execution has its own buffer (so it's thread safe)
19+
20+
# If your change doesn't affect end users or the exported elements of any package,
21+
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
22+
# Optional: The change log or logs in which this entry should be included.
23+
# e.g. '[user]' or '[user, api]'
24+
# Include 'user' if the change is relevant to end users.
25+
# Include 'api' if there is a change to a library API.
26+
# Default: '[user]'
27+
change_logs: []

exporter/awskinesisexporter/exporter.go

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import (
1919
"go.uber.org/zap"
2020

2121
"github.com/open-telemetry/opentelemetry-collector-contrib/exporter/awskinesisexporter/internal/batch"
22-
"github.com/open-telemetry/opentelemetry-collector-contrib/exporter/awskinesisexporter/internal/compress"
2322
"github.com/open-telemetry/opentelemetry-collector-contrib/exporter/awskinesisexporter/internal/producer"
2423
)
2524

@@ -90,16 +89,11 @@ func createExporter(ctx context.Context, c component.Config, log *zap.Logger, op
9089
return nil, err
9190
}
9291

93-
compressor, err := compress.NewCompressor(conf.Encoding.Compression)
94-
if err != nil {
95-
return nil, err
96-
}
97-
9892
encoder, err := batch.NewEncoder(
9993
conf.Encoding.Name,
10094
batch.WithMaxRecordSize(conf.MaxRecordSize),
10195
batch.WithMaxRecordsPerBatch(conf.MaxRecordsPerBatch),
102-
batch.WithCompression(compressor),
96+
batch.WithCompressionType(conf.Compression),
10397
)
10498

10599
if err != nil {

exporter/awskinesisexporter/internal/batch/batch.go

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ type Batch struct {
2929
maxBatchSize int
3030
maxRecordSize int
3131

32-
compression compress.Compressor
32+
compressionType string
3333

3434
records []types.PutRecordsRequestEntry
3535
}
@@ -54,20 +54,18 @@ func WithMaxRecordSize(size int) Option {
5454
}
5555
}
5656

57-
func WithCompression(compressor compress.Compressor) Option {
57+
func WithCompressionType(compressionType string) Option {
5858
return func(bt *Batch) {
59-
if compressor != nil {
60-
bt.compression = compressor
61-
}
59+
bt.compressionType = compressionType
6260
}
6361
}
6462

6563
func New(opts ...Option) *Batch {
6664
bt := &Batch{
67-
maxBatchSize: MaxBatchedRecords,
68-
maxRecordSize: MaxRecordSize,
69-
compression: compress.NewNoopCompressor(),
70-
records: make([]types.PutRecordsRequestEntry, 0, MaxBatchedRecords),
65+
maxBatchSize: MaxBatchedRecords,
66+
maxRecordSize: MaxRecordSize,
67+
compressionType: "none",
68+
records: make([]types.PutRecordsRequestEntry, 0, MaxBatchedRecords),
7169
}
7270

7371
for _, op := range opts {
@@ -78,7 +76,13 @@ func New(opts ...Option) *Batch {
7876
}
7977

8078
func (b *Batch) AddRecord(raw []byte, key string) error {
81-
record, err := b.compression.Do(raw)
79+
80+
compressor, err := compress.NewCompressor(b.compressionType)
81+
if err != nil {
82+
return err
83+
}
84+
85+
record, err := compressor(raw)
8286
if err != nil {
8387
return err
8488
}

exporter/awskinesisexporter/internal/compress/compresser.go

Lines changed: 54 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -9,77 +9,82 @@ import (
99
"compress/gzip"
1010
"compress/zlib"
1111
"fmt"
12-
"io"
1312
)
1413

15-
type bufferedResetWriter interface {
16-
Write(p []byte) (int, error)
17-
Flush() error
18-
Reset(newWriter io.Writer)
19-
Close() error
20-
}
21-
22-
type Compressor interface {
23-
Do(in []byte) (out []byte, err error)
24-
}
25-
26-
var _ Compressor = (*compressor)(nil)
27-
28-
type compressor struct {
29-
compression bufferedResetWriter
30-
}
14+
type Compressor func(in []byte) ([]byte, error)
3115

3216
func NewCompressor(format string) (Compressor, error) {
33-
c := &compressor{
34-
compression: &noop{},
35-
}
3617
switch format {
3718
case "flate":
38-
w, err := flate.NewWriter(nil, flate.BestSpeed)
39-
if err != nil {
40-
return nil, err
41-
}
42-
c.compression = w
19+
return flateCompressor, nil
4320
case "gzip":
44-
w, err := gzip.NewWriterLevel(nil, gzip.BestSpeed)
45-
if err != nil {
46-
return nil, err
47-
}
48-
c.compression = w
49-
21+
return gzipCompressor, nil
5022
case "zlib":
51-
w, err := zlib.NewWriterLevel(nil, zlib.BestSpeed)
52-
if err != nil {
53-
return nil, err
54-
}
55-
c.compression = w
23+
return zlibCompressor, nil
5624
case "noop", "none":
57-
// Already the default case
58-
default:
59-
return nil, fmt.Errorf("unknown compression format: %s", format)
25+
return noopCompressor, nil
26+
}
27+
28+
return nil, fmt.Errorf("unknown compression format: %s", format)
29+
}
30+
31+
func flateCompressor(in []byte) ([]byte, error) {
32+
var buf bytes.Buffer
33+
w, _ := flate.NewWriter(&buf, flate.BestSpeed)
34+
defer w.Close()
35+
36+
_, err := w.Write(in)
37+
38+
if err != nil {
39+
return nil, err
6040
}
6141

62-
return c, nil
42+
err = w.Flush()
43+
if err != nil {
44+
return nil, err
45+
}
46+
47+
return buf.Bytes(), nil
6348
}
6449

65-
func (c *compressor) Do(in []byte) ([]byte, error) {
66-
buf := new(bytes.Buffer)
50+
func gzipCompressor(in []byte) ([]byte, error) {
51+
var buf bytes.Buffer
52+
w, _ := gzip.NewWriterLevel(&buf, gzip.BestSpeed)
53+
defer w.Close()
6754

68-
c.compression.Reset(buf)
55+
_, err := w.Write(in)
6956

70-
if _, err := c.compression.Write(in); err != nil {
57+
if err != nil {
7158
return nil, err
7259
}
7360

74-
if err := c.compression.Flush(); err != nil {
61+
err = w.Flush()
62+
if err != nil {
7563
return nil, err
7664
}
7765

78-
if closer, ok := c.compression.(io.Closer); ok {
79-
if err := closer.Close(); err != nil {
80-
return nil, err
81-
}
66+
return buf.Bytes(), nil
67+
}
68+
69+
func zlibCompressor(in []byte) ([]byte, error) {
70+
var buf bytes.Buffer
71+
w, _ := zlib.NewWriterLevel(&buf, zlib.BestSpeed)
72+
defer w.Close()
73+
74+
_, err := w.Write(in)
75+
76+
if err != nil {
77+
return nil, err
78+
}
79+
80+
err = w.Flush()
81+
if err != nil {
82+
return nil, err
8283
}
8384

8485
return buf.Bytes(), nil
8586
}
87+
88+
func noopCompressor(in []byte) ([]byte, error) {
89+
return in, nil
90+
}

exporter/awskinesisexporter/internal/compress/compresser_test.go

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package compress_test
66
import (
77
"fmt"
88
"math/rand"
9+
"sync"
910
"testing"
1011
"time"
1112

@@ -35,7 +36,7 @@ func TestCompressorFormats(t *testing.T) {
3536
require.NoError(t, err, "Must have a valid compression format")
3637
require.NotNil(t, c, "Must have a valid compressor")
3738

38-
out, err := c.Do([]byte(data))
39+
out, err := c([]byte(data))
3940
assert.NoError(t, err, "Must not error when processing data")
4041
assert.NotNil(t, out, "Must have a valid record")
4142
})
@@ -94,8 +95,86 @@ func benchmarkCompressor(b *testing.B, format string, length int) {
9495
b.ResetTimer()
9596

9697
for i := 0; i < b.N; i++ {
97-
out, err := compressor.Do(data)
98+
out, err := compressor(data)
9899
assert.NoError(b, err, "Must not error when processing data")
99100
assert.NotNil(b, out, "Must have a valid byte array after")
100101
}
101102
}
103+
104+
// an issue encountered in the past was a crash due race condition in the compressor, so the
105+
// current implementation creates a new context on each compression request
106+
// this is a test to check no exceptions are raised for executing concurrent compressions
107+
func TestCompressorConcurrent(t *testing.T) {
108+
109+
timeout := time.After(15 * time.Second)
110+
done := make(chan bool)
111+
go func() {
112+
// do your testing
113+
concurrentCompressFunc(t)
114+
done <- true
115+
}()
116+
117+
select {
118+
case <-timeout:
119+
t.Fatal("Test didn't finish in time")
120+
case <-done:
121+
}
122+
123+
}
124+
125+
func concurrentCompressFunc(t *testing.T) {
126+
// this value should be way higher to make this test more valuable, but the make of this project uses
127+
// max 4 workers, so we had to set this value here
128+
numWorkers := 4
129+
130+
var wg sync.WaitGroup
131+
wg.Add(numWorkers)
132+
133+
errCh := make(chan error, numWorkers)
134+
var errMutex sync.Mutex
135+
136+
// any single format would do it here, since each exporter can be set to use only one at a time
137+
// and the concurrent issue that was present in the past was independent of the format
138+
compressFunc, err := compress.NewCompressor("gzip")
139+
140+
if err != nil {
141+
errCh <- err
142+
return
143+
}
144+
145+
// it is important for the data length to be on the higher side of a record
146+
// since it is where the chances of having race conditions are bigger
147+
dataLength := 131072
148+
149+
for j := 0; j < numWorkers; j++ {
150+
go func() {
151+
defer wg.Done()
152+
153+
source := rand.NewSource(time.Now().UnixMilli())
154+
genRand := rand.New(source)
155+
156+
data := make([]byte, dataLength)
157+
for i := 0; i < dataLength; i++ {
158+
data[i] = byte(genRand.Int31())
159+
}
160+
161+
result, localErr := compressFunc(data)
162+
if localErr != nil {
163+
errMutex.Lock()
164+
errCh <- localErr
165+
errMutex.Unlock()
166+
return
167+
}
168+
169+
_ = result
170+
}()
171+
}
172+
173+
wg.Wait()
174+
175+
close(errCh)
176+
177+
for err := range errCh {
178+
t.Errorf("Error encountered on concurrent compression: %v", err)
179+
}
180+
}

exporter/awskinesisexporter/internal/compress/noop_compression.go

Lines changed: 0 additions & 30 deletions
This file was deleted.

0 commit comments

Comments
 (0)