Skip to content

Commit efb210f

Browse files
committed
fix: put a timeout to callers of methods that use singleflight
1 parent 1df1014 commit efb210f

6 files changed

Lines changed: 64 additions & 12 deletions

File tree

internal/datastore/crdb/crdb.go

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import (
2020
"github.com/shopspring/decimal"
2121
"go.opentelemetry.io/otel"
2222
"golang.org/x/sync/errgroup"
23-
"resenje.org/singleflight"
2423

2524
"github.com/authzed/spicedb/internal/datastore/common"
2625
"github.com/authzed/spicedb/internal/datastore/crdb/migrations"
@@ -278,7 +277,6 @@ type crdbDatastore struct {
278277
beginChangefeedQuery string
279278
transactionNowQuery string
280279

281-
featuresGroup singleflight.Group[string, *datastore.Features]
282280
cachedFeatures *datastore.Features // GUARDED_BY(featuresLock)
283281
featuresLock sync.Mutex
284282

@@ -562,9 +560,7 @@ func (cds *crdbDatastore) Features(ctx context.Context) (*datastore.Features, er
562560
return cached, nil
563561
}
564562

565-
features, _, err := cds.featuresGroup.Do(ctx, "", func(ictx context.Context) (*datastore.Features, error) {
566-
return cds.features(ictx)
567-
})
563+
features, err := cds.features(ctx)
568564
if err != nil {
569565
return nil, err
570566
}
@@ -573,7 +569,7 @@ func (cds *crdbDatastore) Features(ctx context.Context) (*datastore.Features, er
573569
cds.cachedFeatures = features
574570
cds.featuresLock.Unlock()
575571

576-
return features, err
572+
return features, nil
577573
}
578574

579575
func (cds *crdbDatastore) features(ctx context.Context) (*datastore.Features, error) {

internal/datastore/proxy/singleflight.go

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,20 @@ package proxy
22

33
import (
44
"context"
5+
"time"
56

67
"resenje.org/singleflight"
78

89
"github.com/authzed/spicedb/pkg/datastore"
910
"github.com/authzed/spicedb/pkg/datastore/options"
1011
)
1112

13+
// singleflightTimeout is the maximum time that a caller of a singleflighted method
14+
// will wait before giving up on the singleflight executor.
15+
// This prevents a possible deadlock when all datastore connections are held by goroutines waiting on the
16+
// singleflight while the singleflight executor is blocked waiting for a connection.
17+
const singleFlightTimeout = 1 * time.Second
18+
1219
// NewSingleflightDatastoreProxy creates a new Datastore proxy which
1320
// deduplicates calls to Datastore methods that can share results.
1421
func NewSingleflightDatastoreProxy(d datastore.Datastore) datastore.Datastore {
@@ -19,6 +26,7 @@ type singleflightProxy struct {
1926
headRevGroup singleflight.Group[string, datastore.RevisionWithSchemaHash]
2027
checkRevGroup singleflight.Group[string, string]
2128
statsGroup singleflight.Group[string, datastore.Stats]
29+
featuresGroup singleflight.Group[string, *datastore.Features]
2230
delegate datastore.Datastore
2331
}
2432

@@ -43,17 +51,31 @@ func (p *singleflightProxy) ReadWriteTx(ctx context.Context, f datastore.TxUserF
4351
func (p *singleflightProxy) OptimizedRevision(ctx context.Context) (datastore.RevisionWithSchemaHash, error) {
4452
// NOTE: Optimized revisions are singleflighted by the underlying datastore via the
4553
// CachedOptimizedRevisions struct.
54+
ctx, span := tracer.Start(ctx, "singleflightProxy.OptimizedRevision")
55+
defer span.End()
4656
return p.delegate.OptimizedRevision(ctx)
4757
}
4858

4959
func (p *singleflightProxy) CheckRevision(ctx context.Context, revision datastore.Revision) error {
60+
ctx, span := tracer.Start(ctx, "singleflightProxy.CheckRevision")
61+
defer span.End()
62+
63+
ctx, cancel := context.WithTimeout(ctx, singleFlightTimeout)
64+
defer cancel()
65+
5066
_, _, err := p.checkRevGroup.Do(ctx, revision.String(), func(ctx context.Context) (string, error) {
5167
return "", p.delegate.CheckRevision(ctx, revision)
5268
})
5369
return err
5470
}
5571

5672
func (p *singleflightProxy) HeadRevision(ctx context.Context) (datastore.RevisionWithSchemaHash, error) {
73+
ctx, span := tracer.Start(ctx, "singleflightProxy.HeadRevision")
74+
defer span.End()
75+
76+
ctx, cancel := context.WithTimeout(ctx, singleFlightTimeout)
77+
defer cancel()
78+
5779
rev, _, err := p.headRevGroup.Do(ctx, "", func(ctx context.Context) (datastore.RevisionWithSchemaHash, error) {
5880
return p.delegate.HeadRevision(ctx)
5981
})
@@ -69,14 +91,29 @@ func (p *singleflightProxy) Watch(ctx context.Context, afterRevision datastore.R
6991
}
7092

7193
func (p *singleflightProxy) Statistics(ctx context.Context) (datastore.Stats, error) {
94+
ctx, span := tracer.Start(ctx, "singleflightProxy.Statistics")
95+
defer span.End()
96+
97+
ctx, cancel := context.WithTimeout(ctx, singleFlightTimeout)
98+
defer cancel()
99+
72100
stats, _, err := p.statsGroup.Do(ctx, "", func(ctx context.Context) (datastore.Stats, error) {
73101
return p.delegate.Statistics(ctx)
74102
})
75103
return stats, err
76104
}
77105

78106
func (p *singleflightProxy) Features(ctx context.Context) (*datastore.Features, error) {
79-
return p.delegate.Features(ctx)
107+
ctx, span := tracer.Start(ctx, "singleflightProxy.Statistics")
108+
defer span.End()
109+
110+
ctx, cancel := context.WithTimeout(ctx, singleFlightTimeout)
111+
defer cancel()
112+
113+
features, _, err := p.featuresGroup.Do(ctx, "", func(ctx context.Context) (*datastore.Features, error) {
114+
return p.delegate.Features(ctx)
115+
})
116+
return features, err
80117
}
81118

82119
func (p *singleflightProxy) OfflineFeatures() (*datastore.Features, error) {

internal/datastore/revisions/optimized.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ import (
1717
"github.com/authzed/spicedb/pkg/datastore"
1818
)
1919

20+
// singleflightTimeout is the maximum time that a caller of a singleflighted method
21+
// will wait before giving up on the singleflight executor.
22+
// This prevents a possible deadlock when all datastore connections are held by goroutines waiting on the
23+
// singleflight while the singleflight executor is blocked waiting for a connection.
24+
const singleFlightTimeout = 1 * time.Second
25+
2026
var tracer = otel.Tracer("spicedb/internal/datastore/common/revisions")
2127

2228
// OptimizedRevisionFunction instructs the datastore to compute its own current
@@ -64,6 +70,9 @@ func (cor *CachedOptimizedRevisions) OptimizedRevision(ctx context.Context) (dat
6470
}
6571
cor.RUnlock()
6672

73+
ctx, cancel := context.WithTimeout(ctx, singleFlightTimeout)
74+
defer cancel()
75+
6776
result, _, err := cor.updateGroup.Do(ctx, "", func(ctx context.Context) (datastore.RevisionWithSchemaHash, error) {
6877
log.Ctx(ctx).Debug().Time("now", localNow).Msg("computing new revision")
6978

pkg/cmd/server/server_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -471,7 +471,7 @@ func TestServerGracefulTerminationOnError(t *testing.T) {
471471
}, WithPresharedSecureKey("psk"), WithDatastore(ds), WithEnableMemoryProtectionMiddleware(false))
472472
cancel()
473473
_, err = c.Complete(ctx)
474-
require.NoError(t, err)
474+
require.ErrorIs(t, err, context.Canceled)
475475
}
476476

477477
func TestReplaceUnaryMiddleware(t *testing.T) {

pkg/datalayer/hashcache.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ import (
1616

1717
var tracer = otel.Tracer("spicedb/pkg/datalayer")
1818

19-
// singleflightTimeout is the maximum time to wait for a singleflight peer to
20-
// load a schema before falling back to a direct load. This prevents a possible
21-
// deadlock when all connections in a pool are held by goroutines waiting on the
22-
// singleflight while the singleflight leader is blocked waiting for a connection.
19+
// singleflightTimeout is the maximum time that a caller of a singleflighted method
20+
// will wait before giving up on the singleflight executor.
21+
// This prevents a possible deadlock when all datastore connections are held by goroutines waiting on the
22+
// singleflight while the singleflight executor is blocked waiting for a connection.
2323
//
2424
//nolint:revive // var instead of const to allow test overrides
2525
var singleflightTimeout = 1 * time.Second

pkg/datastore/context.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@ package datastore
33
import (
44
"context"
55

6+
"go.opentelemetry.io/otel"
7+
68
"github.com/authzed/spicedb/pkg/datastore/options"
79
core "github.com/authzed/spicedb/pkg/proto/core/v1"
810
)
911

12+
var tracer = otel.Tracer("spicedb/pkg/datastore/context")
13+
1014
// NewSeparatingContextDatastoreProxy severs any timeouts in the context being
1115
// passed to the datastore and only retains tracing metadata.
1216
//
@@ -49,14 +53,20 @@ func (p *ctxProxy) IsStrictReadModeEnabled() bool {
4953
}
5054

5155
func (p *ctxProxy) OptimizedRevision(ctx context.Context) (RevisionWithSchemaHash, error) {
56+
ctx, span := tracer.Start(ctx, "ctxProxy.OptimizedRevision")
57+
defer span.End()
5258
return p.delegate.OptimizedRevision(context.WithoutCancel(ctx))
5359
}
5460

5561
func (p *ctxProxy) CheckRevision(ctx context.Context, revision Revision) error {
62+
ctx, span := tracer.Start(ctx, "ctxProxy.CheckRevision")
63+
defer span.End()
5664
return p.delegate.CheckRevision(context.WithoutCancel(ctx), revision)
5765
}
5866

5967
func (p *ctxProxy) HeadRevision(ctx context.Context) (RevisionWithSchemaHash, error) {
68+
ctx, span := tracer.Start(ctx, "ctxProxy.HeadRevision")
69+
defer span.End()
6070
return p.delegate.HeadRevision(context.WithoutCancel(ctx))
6171
}
6272

0 commit comments

Comments
 (0)