fix: stabilize flaky CI tests

1. Add sync.Mutex to ScoutConnCollected to eliminate data race between
   Add()/MarkWrite() in readLoop and learn() iterating results.
   Introduce Snapshot() for safe read access.

2. Increase bloom filter test size from 500 to 100000 to prevent
   false negatives from random eviction in the stable bloom filter.

3. Use Require().NoError() in TestHTTPSRequest to prevent nil-pointer
   panic on resp.Body.Close() when the request fails.

Fixes #425
This commit is contained in:
Alexey Dolotov
2026-03-30 14:50:32 +03:00
parent a2de52f071
commit e54d9d60d3
4 changed files with 30 additions and 9 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ type StableBloomFilterTestSuite struct {
} }
func (suite *StableBloomFilterTestSuite) TestOp() { func (suite *StableBloomFilterTestSuite) TestOp() {
filter := antireplay.NewStableBloomFilter(500, 0.001) filter := antireplay.NewStableBloomFilter(100000, 0.001)
suite.False(filter.SeenBefore([]byte{1, 2, 3})) suite.False(filter.SeenBefore([]byte{1, 2, 3}))
suite.False(filter.SeenBefore([]byte{4, 5, 6})) suite.False(filter.SeenBefore([]byte{4, 5, 6}))
+8 -6
View File
@@ -61,7 +61,9 @@ func (s Scout) learn(ctx context.Context, url string) (ScoutResult, error) {
client.CloseIdleConnections() client.CloseIdleConnections()
} }
if err != nil || len(results.data) == 0 { data, writeIndex := results.Snapshot()
if err != nil || len(data) == 0 {
return ScoutResult{}, err return ScoutResult{}, err
} }
@@ -70,14 +72,14 @@ func (s Scout) learn(ctx context.Context, url string) (ScoutResult, error) {
// Compute inter-record durations (existing logic). // Compute inter-record durations (existing logic).
lastTimestamp := time.Time{} lastTimestamp := time.Time{}
for i, v := range results.data { for i, v := range data {
if v.recordType != tls.TypeApplicationData { if v.recordType != tls.TypeApplicationData {
continue continue
} }
if lastTimestamp.IsZero() { if lastTimestamp.IsZero() {
if i > 0 { if i > 0 {
lastTimestamp = results.data[i-1].timestamp lastTimestamp = data[i-1].timestamp
} else { } else {
lastTimestamp = v.timestamp lastTimestamp = v.timestamp
} }
@@ -90,12 +92,12 @@ func (s Scout) learn(ctx context.Context, url string) (ScoutResult, error) {
// Compute cert size: sum of ApplicationData payload between CCS and // Compute cert size: sum of ApplicationData payload between CCS and
// the first client Write (which marks the end of server handshake). // the first client Write (which marks the end of server handshake).
seenCCS := false seenCCS := false
boundary := results.writeIndex boundary := writeIndex
if boundary < 0 { if boundary < 0 {
boundary = len(results.data) boundary = len(data)
} }
for i, v := range results.data { for i, v := range data {
if i >= boundary { if i >= boundary {
break break
} }
+20 -1
View File
@@ -1,6 +1,9 @@
package doppel package doppel
import "time" import (
"sync"
"time"
)
const ( const (
ScoutConnCollectedPreallocSize = 100 ScoutConnCollectedPreallocSize = 100
@@ -13,23 +16,39 @@ type ScoutConnResult struct {
} }
type ScoutConnCollected struct { type ScoutConnCollected struct {
mu sync.Mutex
data []ScoutConnResult data []ScoutConnResult
writeIndex int // index at which client first wrote post-handshake data; -1 if not set writeIndex int // index at which client first wrote post-handshake data; -1 if not set
} }
func (s *ScoutConnCollected) Add(record byte, payloadLen int) { func (s *ScoutConnCollected) Add(record byte, payloadLen int) {
s.mu.Lock()
s.data = append(s.data, ScoutConnResult{ s.data = append(s.data, ScoutConnResult{
timestamp: time.Now(), timestamp: time.Now(),
recordType: record, recordType: record,
payloadLen: payloadLen, payloadLen: payloadLen,
}) })
s.mu.Unlock()
} }
// MarkWrite records the current data length as the handshake boundary. // MarkWrite records the current data length as the handshake boundary.
func (s *ScoutConnCollected) MarkWrite() { func (s *ScoutConnCollected) MarkWrite() {
s.mu.Lock()
if s.writeIndex < 0 { if s.writeIndex < 0 {
s.writeIndex = len(s.data) s.writeIndex = len(s.data)
} }
s.mu.Unlock()
}
// Snapshot returns a copy of the collected data and the write index.
func (s *ScoutConnCollected) Snapshot() ([]ScoutConnResult, int) {
s.mu.Lock()
snapshot := make([]ScoutConnResult, len(s.data))
copy(snapshot, s.data)
writeIndex := s.writeIndex
s.mu.Unlock()
return snapshot, writeIndex
} }
func NewScoutConnCollected() *ScoutConnCollected { func NewScoutConnCollected() *ScoutConnCollected {
+1 -1
View File
@@ -175,7 +175,7 @@ func (suite *ProxyTestSuite) TestHTTPSRequest() {
addr := fmt.Sprintf("https://%s/headers", suite.ProxyAddress()) addr := fmt.Sprintf("https://%s/headers", suite.ProxyAddress())
resp, err := client.Get(addr) //nolint: noctx resp, err := client.Get(addr) //nolint: noctx
suite.NoError(err) suite.Require().NoError(err)
defer resp.Body.Close() //nolint: errcheck defer resp.Body.Close() //nolint: errcheck