@@ -7,11 +7,11 @@ package profiler
77
88import (
99 "bytes"
10- "compress/gzip"
1110 "fmt"
1211 "io"
1312 "os"
1413 "testing"
14+ "testing/synctest"
1515 "time"
1616
1717 kgzip "github.com/klauspost/compress/gzip"
@@ -58,7 +58,79 @@ func TestNewCompressionPipeline(t *testing.T) {
5858 }
5959}
6060
61- // checkZstdLevel checks that data is zstd-compressed with the given level
61+ // TestGzipDecodingRecompressorInvalidInput verifies that the recompressor
62+ // surfaces an error (instead of deadlocking) when its input is not a valid
63+ // gzip stream. A naive implementation spawns a goroutine that calls
64+ // kgzip.NewReader, which fails on a bad header. Without explicitly closing
65+ // the read end of the pipe, the goroutine exits while the caller's pw.Write
66+ // blocks forever waiting for a reader that no longer exists.
67+ //
68+ // The test runs inside a testing/synctest bubble so that deadlock detection
69+ // is deterministic instead of relying on a wall-clock timeout: synctest.Wait
70+ // returns once every goroutine in the bubble is durably blocked or has
71+ // exited, and we then assert that Write actually returned.
72+ func TestGzipDecodingRecompressorInvalidInput (t * testing.T ) {
73+ synctest .Test (t , func (t * testing.T ) {
74+ gzipOut , err := kgzip .NewWriterLevel (nil , gzip6Compression .level )
75+ require .NoError (t , err )
76+ r := newGzipDecodingRecompressor (gzipOut )
77+ r .Reset (io .Discard )
78+ // Always close so the synctest bubble drains cleanly even
79+ // when the recompressor is deadlocked: pw.Close unblocks
80+ // any pending Write, and <-r.err unblocks the goroutine
81+ // started by Reset.
82+ defer r .Close ()
83+
84+ // Non-gzip data large enough to overflow whatever buffer
85+ // kgzip.NewReader uses internally for header parsing. With
86+ // io.Pipe being unbuffered, any write left over after the
87+ // recompressor's goroutine errors out will block forever
88+ // absent a fix.
89+ data := bytes .Repeat ([]byte ("not a gzip stream\n " ), 4096 )
90+
91+ writeDone := make (chan error , 1 )
92+ go func () {
93+ _ , werr := r .Write (data )
94+ writeDone <- werr
95+ }()
96+
97+ // Block until every goroutine in the bubble is either
98+ // durably blocked or has exited. With the fix in place,
99+ // the recompressor's goroutine closes pr with an error,
100+ // Write returns that error, and writeDone receives a
101+ // value. Without the fix, Write stays blocked on the
102+ // pipe and writeDone is empty.
103+ synctest .Wait ()
104+
105+ select {
106+ case werr := <- writeDone :
107+ require .Error (t , werr , "expected an error for non-gzip input" )
108+ default :
109+ t .Error ("deadlock: Write did not return after recompressor goroutine exited" )
110+ }
111+ })
112+ }
113+
114+ // checkGzipLevel checks that data is gzip-compressed with the given level.
115+ func checkGzipLevel (t * testing.T , data []byte , level int ) {
116+ t .Helper ()
117+ require .NotEmpty (t , data )
118+ gr , err := kgzip .NewReader (bytes .NewReader (data ))
119+ require .NoError (t , err )
120+ in := new (bytes.Buffer )
121+ _ , err = io .Copy (in , gr )
122+ require .NoError (t , err )
123+ require .NoError (t , gr .Close ())
124+ out := new (bytes.Buffer )
125+ gw , err := kgzip .NewWriterLevel (out , level )
126+ require .NoError (t , err )
127+ _ , err = io .Copy (gw , in )
128+ require .NoError (t , err )
129+ require .NoError (t , gw .Close ())
130+ require .Equal (t , data , out .Bytes ())
131+ }
132+
133+ // checkZstdLevel checks that data is zstd-compressed with the given level.
62134func checkZstdLevel (t * testing.T , data []byte , level zstd.EncoderLevel ) {
63135 t .Helper ()
64136 require .NotEmpty (t , data )
@@ -95,19 +167,19 @@ func TestDebugCompressionEnv(t *testing.T) {
95167 t .Run ("explicit-gzip" , func (t * testing.T ) {
96168 t .Setenv ("DD_PROFILING_DEBUG_COMPRESSION_SETTINGS" , "gzip" )
97169 p := startTestProfiler (t , 1 , WithProfileTypes (HeapProfile , BlockProfile ), WithPeriod (time .Millisecond )).ReceiveProfile (t )
98- r , err := gzip .NewReader (bytes .NewReader (p .attachments ["delta-heap.pprof" ]))
99- require .NoError (t , err )
100- _ , err = io .Copy (io .Discard , r )
101- require .NoError (t , err )
170+ checkGzipLevel (t , p .attachments ["delta-heap.pprof" ], gzip6Compression .level )
102171 })
103172
104173 t .Run ("explicit-gzip-already-gzipped-input" , func (t * testing.T ) {
105174 t .Setenv ("DD_PROFILING_DEBUG_COMPRESSION_SETTINGS" , "gzip" )
106175 p := startTestProfiler (t , 1 , WithProfileTypes (CPUProfile ), WithPeriod (time .Millisecond )).ReceiveProfile (t )
107- r , err := gzip .NewReader (bytes .NewReader (p .attachments ["cpu.pprof" ]))
108- require .NoError (t , err )
109- _ , err = io .Copy (io .Discard , r )
110- require .NoError (t , err )
176+ checkGzipLevel (t , p .attachments ["cpu.pprof" ], gzip6Compression .level )
177+ })
178+
179+ t .Run ("gzip-1" , func (t * testing.T ) {
180+ t .Setenv ("DD_PROFILING_DEBUG_COMPRESSION_SETTINGS" , "gzip-1" )
181+ p := startTestProfiler (t , 1 , WithProfileTypes (HeapProfile ), WithPeriod (time .Millisecond )).ReceiveProfile (t )
182+ checkGzipLevel (t , p .attachments ["delta-heap.pprof" ], gzip1Compression .level )
111183 })
112184
113185 t .Run ("zstd-delta" , func (t * testing.T ) {
@@ -192,7 +264,7 @@ func BenchmarkRecompression(b *testing.B) {
192264 if err != nil {
193265 b .Fatal (err )
194266 }
195- z := newZstdRecompressor (encoder )
267+ z := newGzipDecodingRecompressor (encoder )
196268 z .Reset (io .Discard )
197269 if _ , err := z .Write (data ); err != nil {
198270 b .Fatal (err )
0 commit comments