Skip to content

Commit 6dd9a8c

Browse files
James9074tstirrat15
authored andcommitted
chore: go-require: require must only be used in the goroutine running the test function (testifylint) - Move require.Error outside of goroutine
1 parent c39bf5f commit 6dd9a8c

File tree

4 files changed

+83
-64
lines changed

4 files changed

+83
-64
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ require (
211211
github.com/butuzov/mirror v1.3.0 // indirect
212212
github.com/catenacyber/perfsprint v0.9.1 // indirect
213213
github.com/ccojocar/zxcvbn-go v1.0.4 // indirect
214+
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
214215
github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d // indirect
215216
github.com/charithe/durationcheck v0.0.10 // indirect
216217
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1534,6 +1534,8 @@ github.com/ccoveille/go-safecast v1.6.1 h1:Nb9WMDR8PqhnKCVs2sCB+OqhohwO5qaXtCviZ
15341534
github.com/ccoveille/go-safecast v1.6.1/go.mod h1:QqwNjxQ7DAqY0C721OIO9InMk9zCwcsO7tnRuHytad8=
15351535
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
15361536
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
1537+
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
1538+
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
15371539
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
15381540
github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
15391541
github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=

internal/datastore/common/gc.go

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import (
55
"fmt"
66
"time"
77

8-
"github.com/cenkalti/backoff/v4"
8+
"github.com/cenkalti/backoff/v5"
99
"github.com/prometheus/client_golang/prometheus"
1010
"github.com/rs/zerolog"
1111

@@ -148,14 +148,17 @@ var MaxGCInterval = 60 * time.Minute
148148
// StartGarbageCollector loops forever until the context is canceled and
149149
// performs garbage collection on the provided interval.
150150
func StartGarbageCollector(ctx context.Context, collectable GarbageCollectableDatastore, interval, window, timeout time.Duration) error {
151-
return startGarbageCollectorWithMaxElapsedTime(ctx, collectable, interval, window, 0, timeout, gcFailureCounter)
151+
return runPeriodicallyWithBackoff(ctx, func() error {
152+
gcCtx, cancel := context.WithTimeout(ctx, timeout)
153+
defer cancel()
154+
return RunGarbageCollection(gcCtx, collectable, window)
155+
}, interval, window, timeout, gcFailureCounter)
152156
}
153157

154-
func startGarbageCollectorWithMaxElapsedTime(ctx context.Context, collectable GarbageCollectableDatastore, interval, window, maxElapsedTime, timeout time.Duration, failureCounter prometheus.Counter) error {
158+
func runPeriodicallyWithBackoff(ctx context.Context, taskFn func() error, interval, window, timeout time.Duration, failureCounter prometheus.Counter) error {
155159
backoffInterval := backoff.NewExponentialBackOff()
156160
backoffInterval.InitialInterval = interval
157161
backoffInterval.MaxInterval = max(MaxGCInterval, interval)
158-
backoffInterval.MaxElapsedTime = maxElapsedTime
159162
backoffInterval.Reset()
160163

161164
nextInterval := interval
@@ -178,7 +181,10 @@ func startGarbageCollectorWithMaxElapsedTime(ctx context.Context, collectable Ga
178181
Dur("timeout", timeout).
179182
Msg("running garbage collection worker")
180183

181-
err := RunGarbageCollection(collectable, window, timeout)
184+
// NOTE: we're okay using the parent context here because the
185+
// callers of this function create a dedicated garbage collection
186+
// context anyway, which is only cancelled when the ds is closed.
187+
err := taskFn()
182188
if err != nil {
183189
failureCounter.Inc()
184190
nextInterval = backoffInterval.NextBackOff()
@@ -199,10 +205,7 @@ func startGarbageCollectorWithMaxElapsedTime(ctx context.Context, collectable Ga
199205
}
200206

201207
// RunGarbageCollection runs garbage collection for the datastore.
202-
func RunGarbageCollection(collectable GarbageCollectableDatastore, window, timeout time.Duration) error {
203-
ctx, cancel := context.WithTimeout(context.Background(), timeout)
204-
defer cancel()
205-
208+
func RunGarbageCollection(ctx context.Context, collectable GarbageCollectableDatastore, window time.Duration) error {
206209
ctx, span := tracer.Start(ctx, "RunGarbageCollection")
207210
defer span.End()
208211

internal/datastore/common/gc_test.go

Lines changed: 68 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,18 @@ package common
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67
"slices"
78
"sync"
89
"testing"
10+
"testing/synctest"
911
"time"
1012

1113
"github.com/prometheus/client_golang/prometheus"
1214
promclient "github.com/prometheus/client_model/go"
1315
"github.com/stretchr/testify/require"
16+
"go.uber.org/goleak"
1417

1518
"github.com/authzed/spicedb/internal/datastore/revisions"
1619
"github.com/authzed/spicedb/pkg/datastore"
@@ -181,50 +184,45 @@ func (d revisionErrorDeleter) DeleteExpiredRels() (int64, error) {
181184
return 0, nil
182185
}
183186

187+
func alwaysErr() error {
188+
return errors.New("aaagh")
189+
}
190+
184191
func TestGCFailureBackoff(t *testing.T) {
192+
t.Cleanup(func() {
193+
goleak.VerifyNone(t)
194+
})
185195
localCounter := prometheus.NewCounter(gcFailureCounterConfig)
186196
reg := prometheus.NewRegistry()
187197
require.NoError(t, reg.Register(localCounter))
188198

189-
ctx, cancel := context.WithCancel(t.Context())
190-
defer cancel()
191-
go func() {
192-
gc := newFakeGCStore(alwaysErrorDeleter{})
193-
require.Error(t, startGarbageCollectorWithMaxElapsedTime(ctx, gc, 100*time.Millisecond, 1*time.Second, 1*time.Nanosecond, 1*time.Minute, localCounter))
194-
}()
195-
time.Sleep(200 * time.Millisecond)
196-
cancel()
199+
errCh := make(chan error, 1)
200+
synctest.Test(t, func(t *testing.T) {
201+
duration := 1000 * time.Second
202+
ctx, cancel := context.WithTimeout(t.Context(), duration)
203+
t.Cleanup(func() {
204+
cancel()
205+
})
206+
go func() {
207+
errCh <- runPeriodicallyWithBackoff(ctx, alwaysErr, 100*time.Second, 1*time.Second, 1*time.Minute, localCounter)
208+
}()
209+
time.Sleep(duration)
210+
synctest.Wait()
211+
})
212+
require.Error(t, <-errCh)
197213

198214
metrics, err := reg.Gather()
199215
require.NoError(t, err)
200216
var mf *promclient.MetricFamily
201217
for _, metric := range metrics {
202218
if metric.GetName() == "spicedb_datastore_gc_failure_total" {
203219
mf = metric
220+
break
204221
}
205222
}
206-
require.Greater(t, *(mf.GetMetric()[0].Counter.Value), 100.0, "MaxElapsedTime=1ns did not cause backoff to get ignored")
207-
208-
localCounter = prometheus.NewCounter(gcFailureCounterConfig)
209-
reg = prometheus.NewRegistry()
210-
require.NoError(t, reg.Register(localCounter))
211-
ctx, cancel = context.WithCancel(t.Context())
212-
defer cancel()
213-
go func() {
214-
gc := newFakeGCStore(alwaysErrorDeleter{})
215-
require.Error(t, startGarbageCollectorWithMaxElapsedTime(ctx, gc, 100*time.Millisecond, 0, 1*time.Second, 1*time.Minute, localCounter))
216-
}()
217-
time.Sleep(200 * time.Millisecond)
218-
cancel()
219-
220-
metrics, err = reg.Gather()
221-
require.NoError(t, err)
222-
for _, metric := range metrics {
223-
if metric.GetName() == "spicedb_datastore_gc_failure_total" {
224-
mf = metric
225-
}
226-
}
227-
require.Less(t, *(mf.GetMetric()[0].Counter.Value), 3.0, "MaxElapsedTime=0 should have not caused backoff to get ignored")
223+
// We expect about 5 failures; the behavior of the library means that there's some wiggle room here.
224+
// (owing to the jitter in the backoff)
225+
require.Greater(t, *(mf.GetMetric()[0].Counter.Value), 3.0, "did not see expected number of backoffs")
228226
}
229227

230228
// Ensure the garbage collector interval is reset after recovering from an
@@ -238,19 +236,25 @@ func TestGCFailureBackoffReset(t *testing.T) {
238236
errorOnRevisions: []uint64{1, 2, 3, 4, 5},
239237
})
240238

241-
ctx, cancel := context.WithCancel(t.Context())
242-
defer cancel()
243-
244-
go func() {
245-
interval := 10 * time.Millisecond
246-
window := 10 * time.Second
247-
timeout := 1 * time.Minute
248-
249-
require.Error(t, StartGarbageCollector(ctx, gc, interval, window, timeout))
250-
}()
239+
errCh := make(chan error, 1)
240+
synctest.Test(t, func(t *testing.T) {
241+
ctx, cancel := context.WithCancel(t.Context())
242+
t.Cleanup(func() {
243+
cancel()
244+
})
245+
go func() {
246+
interval := 10 * time.Millisecond
247+
window := 10 * time.Second
248+
timeout := 1 * time.Minute
249+
250+
errCh <- StartGarbageCollector(ctx, gc, interval, window, timeout)
251+
}()
252+
time.Sleep(500 * time.Millisecond)
253+
cancel()
254+
synctest.Wait()
255+
})
251256

252-
time.Sleep(500 * time.Millisecond)
253-
cancel()
257+
require.Error(t, <-errCh)
254258

255259
// The next interval should have been reset after recovering from the error.
256260
// If it is not reset, the last exponential backoff interval will not give
@@ -264,20 +268,29 @@ func TestGCFailureBackoffReset(t *testing.T) {
264268
func TestGCUnlockOnTimeout(t *testing.T) {
265269
gc := newFakeGCStore(alwaysErrorDeleter{})
266270

267-
ctx, cancel := context.WithCancel(t.Context())
268-
defer cancel()
269-
270-
go func() {
271-
interval := 10 * time.Millisecond
272-
window := 10 * time.Second
273-
timeout := 1 * time.Millisecond
274-
275-
require.Error(t, StartGarbageCollector(ctx, gc, interval, window, timeout))
276-
}()
277-
278-
time.Sleep(30 * time.Millisecond)
279-
require.False(t, gc.HasGCRun(), "GC should not have run")
271+
errCh := make(chan error, 1)
272+
hasRunChan := make(chan bool, 1)
273+
synctest.Test(t, func(t *testing.T) {
274+
ctx, cancel := context.WithCancel(t.Context())
275+
t.Cleanup(func() {
276+
cancel()
277+
})
278+
go func() {
279+
interval := 10 * time.Millisecond
280+
window := 10 * time.Second
281+
timeout := 1 * time.Minute
282+
283+
errCh <- StartGarbageCollector(ctx, gc, interval, window, timeout)
284+
}()
285+
time.Sleep(30 * time.Millisecond)
286+
hasRunChan <- gc.HasGCRun()
287+
cancel()
288+
synctest.Wait()
289+
})
290+
require.Error(t, <-errCh)
291+
require.False(t, <-hasRunChan, "GC should not have run")
280292

293+
// TODO: should this be inside the goroutine as well?
281294
gc.fakeGC.lock.Lock()
282295
defer gc.fakeGC.lock.Unlock()
283296

0 commit comments

Comments
 (0)