Skip to content

Commit c77e4fa

Browse files
authored
fix(datastores): ReadyState respects context cancellation/deadline (#3262)
1 parent f121064 commit c77e4fa

18 files changed

Lines changed: 287 additions & 31 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
88
- Postgres: read replicas no longer intermittently return `object definition not found` under load. The strict read-replica guard now verifies that the replica's snapshot has caught up to the revision being read (snapshot domination) instead of checking a single transaction id, and raises from within the read itself rather than from a trailing assertion, so a replica that catches up mid-query can no longer let an incomplete read through. In both cases the read correctly falls back to the primary. (https://github.com/authzed/spicedb/pull/3243)
99
- Prevent ReadRelationships from doing work that's immediately discarded when the `optional_limit` parameter is used (https://github.com/authzed/spicedb/pull/3253)
1010
- MemDB: overlapping write transactions could violate every snapshot-consistency invariant of the datastore — a committed write could be invisible at its own returned revision (breaking read-your-writes, e.g. an at-exact-snapshot `Check` right after `WriteRelationships`), a later commit could leak into reads at an earlier revision, a write visible at one revision could be missing at a later one (including head), and two concurrent transactions could be assigned the same revision. Revisions are now assigned at write-transaction acquisition, where writer serialization makes the two orders identical. (https://github.com/authzed/spicedb/pull/3239)
11+
- All datastores now answer Ready (or not) within a bounded time (https://github.com/authzed/spicedb/pull/3262)
1112

1213
## [1.56.0] - 2026-07-24
1314
### Added

internal/datastore/crdb/crdb.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -90,17 +90,17 @@ func newCRDBDatastore(ctx context.Context, url string, options ...Option) (datas
9090
return nil, common.RedactAndLogSensitiveConnString(ctx, errUnableToInstantiate, err, url)
9191
}
9292

93-
initCtx, initCancel := context.WithTimeout(context.Background(), 5*time.Minute)
94-
defer initCancel()
95-
9693
healthChecker, err := pool.NewNodeHealthChecker(url)
9794
if err != nil {
9895
return nil, common.RedactAndLogSensitiveConnString(ctx, errUnableToInstantiate, err, url)
9996
}
10097

10198
// The initPool is a 1-connection pool that is only used for setup tasks.
102-
// The actual pools are not given the initCtx, since cancellation can
103-
// interfere with pool setup.
99+
// If the database is completely unreachable (e.g. wrong credentials),
100+
// this will block for 15 seconds and then error out.
101+
// TODO(miparnisari): remove this initCtx once spicedb has a k8s startup probe. It will become unnecessary.
102+
initCtx, initCancel := context.WithTimeout(context.Background(), 15*time.Second)
103+
defer initCancel()
104104
initPoolConfig := readPoolConfig.Copy()
105105
initPoolConfig.MinConns = 1
106106
initPool, err := pool.NewRetryPool(initCtx, "init", initPoolConfig, healthChecker, config.maxRetries, config.connectRate)
@@ -208,7 +208,8 @@ func newCRDBDatastore(ctx context.Context, url string, options ...Option) (datas
208208
ds.SetNowFunc(ds.headRevisionInternal)
209209
ds.SetNowOnlyFunc(ds.headRevisionInternalNoHash)
210210

211-
// this ctx and cancel is tied to the lifetime of the datastore
211+
// The actual pools are not given the initCtx.
212+
// This ctx and cancel is tied to the lifetime of the datastore
212213
ds.ctx, ds.cancel = context.WithCancel(context.Background())
213214
ds.writePool, err = pool.NewRetryPool(ds.ctx, "write", writePoolConfig, healthChecker, config.maxRetries, config.connectRate)
214215
if err != nil {
@@ -426,7 +427,7 @@ func wrapError(err error) error {
426427
// to be ready to receive traffic, and total connections counts connections in the constructing
427428
// state, which cannot receive traffic.
428429
func (cds *crdbDatastore) ReadyState(ctx context.Context) (datastore.ReadyState, error) {
429-
currentRevision, err := migrations.NewCRDBDriver(cds.dburl)
430+
currentRevision, err := migrations.NewCRDBDriver(ctx, cds.dburl)
430431
if err != nil {
431432
return datastore.ReadyState{}, err
432433
}

internal/datastore/crdb/crdb_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ func crdbTestVersion() string {
6767
func TestCRDBDatastoreWithoutIntegrity(t *testing.T) {
6868
t.Parallel()
6969
b := testdatastore.RunCRDBForTesting(t, crdbTestVersion())
70-
test.All(t, crdbFactory.NewTester(test.DatastoreTesterFunc(func(t testing.TB, revisionParameters test.RevisionParameters, watchBufferLength uint16) (datastore.Datastore, error) {
70+
test.All(t, crdbFactory.NewTester(test.PausableTester(test.DatastoreTesterFunc(func(t testing.TB, revisionParameters test.RevisionParameters, watchBufferLength uint16) (datastore.Datastore, error) {
7171
ctx := t.Context()
7272
ds := b.NewDatastore(t, func(engine, uri string) datastore.Datastore {
7373
ds, err := NewCRDBDatastore(
@@ -88,7 +88,7 @@ func TestCRDBDatastoreWithoutIntegrity(t *testing.T) {
8888
})
8989

9090
return ds, nil
91-
})))
91+
}), b)))
9292

9393
t.Run("TestWatchStreaming", createDatastoreTest(
9494
b,
@@ -209,7 +209,7 @@ func TestCRDBDatastoreWithIntegrity(t *testing.T) { //nolint:tparallel
209209
t.Parallel()
210210
b := testdatastore.RunCRDBForTesting(t, crdbTestVersion())
211211

212-
test.AllWithExceptions(t, crdbFactory.NewTester(test.DatastoreTesterFunc(func(t testing.TB, revisionParameters test.RevisionParameters, watchBufferLength uint16) (datastore.Datastore, error) {
212+
test.AllWithExceptions(t, crdbFactory.NewTester(test.PausableTester(test.DatastoreTesterFunc(func(t testing.TB, revisionParameters test.RevisionParameters, watchBufferLength uint16) (datastore.Datastore, error) {
213213
ctx := t.Context()
214214
ds := b.NewDatastore(t, func(engine, uri string) datastore.Datastore {
215215
ds, err := NewCRDBDatastore(
@@ -234,7 +234,7 @@ func TestCRDBDatastoreWithIntegrity(t *testing.T) { //nolint:tparallel
234234
})
235235

236236
return ds, nil
237-
})), test.WithCategories(test.MigrationCategory))
237+
}), b)), test.WithCategories(test.MigrationCategory))
238238

239239
unwrappedTester := test.DatastoreTesterFunc(func(t testing.TB, revisionParameters test.RevisionParameters, watchBufferLength uint16) (datastore.Datastore, error) {
240240
ctx := t.Context()
@@ -311,7 +311,7 @@ func TestWatchFeatureDetection(t *testing.T) {
311311
ctx := t.Context()
312312
adminConn, connStrings := newCRDBWithUser(t)
313313

314-
migrationDriver, err := crdbmigrations.NewCRDBDriver(connStrings[testuser])
314+
migrationDriver, err := crdbmigrations.NewCRDBDriver(ctx, connStrings[testuser])
315315
require.NoError(t, err)
316316
require.NoError(t, crdbmigrations.CRDBMigrations.Run(ctx, migrationDriver, migrate.Head, migrate.LiveRun))
317317

internal/datastore/crdb/enginebuilder.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ func init() {
1818
migration.RegisterMigratableEngine(Engine, migrations.CRDBMigrations, newMigrationDriverFromConfig, "add-schema-tables")
1919
}
2020

21-
func newMigrationDriverFromConfig(_ context.Context, cfg *migration.Config) (*migrations.CRDBDriver, error) {
22-
return migrations.NewCRDBDriver(cfg.DatastoreURI)
21+
func newMigrationDriverFromConfig(ctx context.Context, cfg *migration.Config) (*migrations.CRDBDriver, error) {
22+
return migrations.NewCRDBDriver(ctx, cfg.DatastoreURI)
2323
}
2424

2525
func newDatastoreFromConfig(ctx context.Context, opts datastorecfg.Config) (datastore.Datastore, error) {

internal/datastore/crdb/migrations/driver.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,15 @@ type CRDBDriver struct {
2929

3030
// NewCRDBDriver creates a new driver with active connections to the database
3131
// specified.
32-
func NewCRDBDriver(url string) (*CRDBDriver, error) {
32+
func NewCRDBDriver(ctx context.Context, url string) (*CRDBDriver, error) {
3333
connConfig, err := pgx.ParseConfig(url)
3434
if err != nil {
3535
return nil, fmt.Errorf(errUnableToInstantiate, err)
3636
}
3737
pgxcommon.ConfigurePGXLogger(connConfig)
3838
pgxcommon.ConfigureOTELTracer(connConfig, false)
3939

40-
db, err := pgx.ConnectConfig(context.Background(), connConfig)
40+
db, err := pgx.ConnectConfig(ctx, connConfig)
4141
if err != nil {
4242
return nil, fmt.Errorf(errUnableToInstantiate, err)
4343
}

internal/datastore/mysql/datastore_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ func TestMySQLDatastoreDSNWithoutParseTime(t *testing.T) {
120120
func TestMySQL8Datastore(t *testing.T) {
121121
b := testdatastore.RunMySQLForTestingWithOptions(t, testdatastore.MySQLTesterOptions{MigrateForNewDatastore: true})
122122
dst := datastoreTester{b: b}
123-
test.AllWithExceptions(t, mysqlFactory.NewTester(test.DatastoreTesterFunc(dst.createDatastore)), test.WithCategories(test.WatchSchemaCategory))
123+
test.AllWithExceptions(t, mysqlFactory.NewTester(test.PausableTester(test.DatastoreTesterFunc(dst.createDatastore), b)), test.WithCategories(test.WatchSchemaCategory))
124124
additionalMySQLTests(t, b)
125125
}
126126

internal/datastore/postgres/postgres_shared_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ func testPostgresDatastore(t *testing.T, config postgresTestConfig) {
110110
b := testdatastore.RunPostgresForTesting(t, config.pgVersion, config.pgbouncer)
111111
ctx := t.Context()
112112

113-
test.AllWithExceptions(t, pgFactory.NewTester(test.DatastoreTesterFunc(func(t testing.TB, revisionParameters test.RevisionParameters, watchBufferLength uint16) (datastore.Datastore, error) {
113+
test.AllWithExceptions(t, pgFactory.NewTester(test.PausableTester(test.DatastoreTesterFunc(func(t testing.TB, revisionParameters test.RevisionParameters, watchBufferLength uint16) (datastore.Datastore, error) {
114114
ds := b.NewDatastore(t, func(engine, uri string) datastore.Datastore {
115115
ds, err := newPostgresDatastore(ctx, uri, primaryInstanceID,
116116
RevisionQuantization(revisionParameters.Quantization),
@@ -125,7 +125,7 @@ func testPostgresDatastore(t *testing.T, config postgresTestConfig) {
125125
return indexcheck.WrapWithIndexCheckingDatastoreProxyIfApplicable(ds)
126126
})
127127
return ds, nil
128-
})), test.WithCategories(test.GCCategory))
128+
}), b)), test.WithCategories(test.GCCategory))
129129

130130
t.Run("TransactionTimestamps", createDatastoreTest(
131131
b,
@@ -320,7 +320,7 @@ func testPostgresDatastoreWithoutCommitTimestamps(t *testing.T, config postgresT
320320

321321
// NOTE: watch API requires the commit timestamps, so we skip those tests here.
322322
// NOTE: gc tests take exclusive locks, so they are run under non-parallel.
323-
test.AllWithExceptions(t, pgFactory.NewTester(test.DatastoreTesterFunc(func(t testing.TB, revisionParameters test.RevisionParameters, watchBufferLength uint16) (datastore.Datastore, error) {
323+
test.AllWithExceptions(t, pgFactory.NewTester(test.PausableTester(test.DatastoreTesterFunc(func(t testing.TB, revisionParameters test.RevisionParameters, watchBufferLength uint16) (datastore.Datastore, error) {
324324
ds := b.NewDatastore(t, func(engine, uri string) datastore.Datastore {
325325
ds, err := newPostgresDatastore(ctx, uri, primaryInstanceID,
326326
RevisionQuantization(revisionParameters.Quantization),
@@ -334,7 +334,7 @@ func testPostgresDatastoreWithoutCommitTimestamps(t *testing.T, config postgresT
334334
return ds
335335
})
336336
return ds, nil
337-
})), test.WithCategories(test.WatchCategory, test.GCCategory, test.MigrationCategory))
337+
}), b)), test.WithCategories(test.WatchCategory, test.GCCategory, test.MigrationCategory))
338338
})
339339

340340
t.Run(fmt.Sprintf("postgres-%s-gc", pgVersion), func(t *testing.T) {

internal/datastore/spanner/spanner_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ func TestSpannerDatastore(t *testing.T) {
2929
b := testdatastore.RunSpannerForTesting(t)
3030

3131
// Transaction tests are excluded because, for reasons unknown, one cannot read its own write in one transaction in the Spanner emulator.
32-
test.AllWithExceptions(t, spannerFactory.NewTester(test.DatastoreTesterFunc(func(t testing.TB, revisionParameters test.RevisionParameters, watchBufferLength uint16) (datastore.Datastore, error) {
32+
test.AllWithExceptions(t, spannerFactory.NewTester(test.PausableTester(test.DatastoreTesterFunc(func(t testing.TB, revisionParameters test.RevisionParameters, watchBufferLength uint16) (datastore.Datastore, error) {
3333
ds := b.NewDatastore(t, func(engine, uri string) datastore.Datastore {
3434
ds, err := NewSpannerDatastore(ctx, uri,
3535
RevisionQuantization(revisionParameters.Quantization),
@@ -43,7 +43,7 @@ func TestSpannerDatastore(t *testing.T) {
4343
return ds
4444
})
4545
return ds, nil
46-
})), test.WithCategories(test.GCCategory, test.StatsCategory, test.TransactionCategory))
46+
}), b)), test.WithCategories(test.GCCategory, test.StatsCategory, test.TransactionCategory))
4747

4848
t.Run("TestFakeStats", createDatastoreTest(
4949
b,

internal/services/health/health.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ func NewHealthManager(dispatcher dispatch.Dispatcher, dsc DatastoreChecker) Mana
2828
// traffic.
2929
type DatastoreChecker interface {
3030
// ReadyState returns whether the datastore is ready to be used.
31+
// This call must respect context cancellation and deadlines.
3132
ReadyState(ctx context.Context) (datastore.ReadyState, error)
3233
}
3334

@@ -40,8 +41,15 @@ type Manager interface {
4041
// HealthSvc is the health service this manager is managing.
4142
HealthSvc() *grpcutil.AuthlessHealthServer
4243

43-
// Checker blocks until the status is SERVING.
44+
// Checker blocks until the status is SERVING or until the context is done.
4445
Checker(ctx context.Context) error
46+
47+
// right now, Checker is serving both as a startup probe and a readiness probe.
48+
// However, its implementation is really only functioning as a startup probe because the function exits quickly.
49+
// TODO(miparnisari): Split Checker into two functions: Startup and Readiness (to match kubernetes probes)
50+
// Startup can continue with this implementation. Readiness can ... be something else (TBD).
51+
// ALso: we don't need a per-service probe, just one a general one for the entire service.
52+
// Also: we need a function Close() that sets status to NOT_SERVING.
4553
}
4654

4755
type healthManager struct {
@@ -60,7 +68,11 @@ func (hm *healthManager) RegisterReportedService(serviceName string) {
6068
hm.healthSvc.SetServingStatus(serviceName, healthpb.HealthCheckResponse_NOT_SERVING)
6169
}
6270

71+
// Checker blocks until the underlying dependencies are ready.
72+
// When they are, it marks the service as "serving" and returns.
73+
// If they never become ready, it just returns.
6374
func (hm *healthManager) Checker(ctx context.Context) error {
75+
log.Ctx(ctx).Info().Msg("HEALTHCHECK")
6476
// Run immediately for the initial check
6577
backoffInterval := backoff.NewExponentialBackOff()
6678

@@ -96,6 +108,7 @@ func (hm *healthManager) Checker(ctx context.Context) error {
96108
}
97109
}
98110

111+
// checkIsReady returns true if both the datastore and the dispatcher are ready
99112
func (hm *healthManager) checkIsReady(ctx context.Context) bool {
100113
log.Ctx(ctx).Debug().Msg("checking if datastore and dispatcher are ready")
101114

internal/testserver/datastore/crdb.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@ import (
1818

1919
// crdbTester is safe for concurrent use by tests.
2020
type crdbTester struct {
21+
pausableContainer
22+
2123
// endpoint is the host:port of the cockroach container.
2224
endpoint string
2325
}
2426

25-
var _ RunningEngineForTest = (*crdbTester)(nil)
27+
var _ PausableEngineForTest = (*crdbTester)(nil)
2628

2729
// RunCRDBForTesting returns a RunningEngineForTest for CRDB
2830
func RunCRDBForTesting(t testing.TB, crdbVersion string, opts ...testcontainers.ContainerCustomizer) *crdbTester {
@@ -56,7 +58,8 @@ func RunCRDBForTesting(t testing.TB, crdbVersion string, opts ...testcontainers.
5658
require.NoError(t, err)
5759

5860
return &crdbTester{
59-
endpoint: net.JoinHostPort(host, mappedPort.Port()),
61+
pausableContainer: pausableContainer{container: container},
62+
endpoint: net.JoinHostPort(host, mappedPort.Port()),
6063
}
6164
}
6265

@@ -88,7 +91,7 @@ func (r *crdbTester) NewDatabase(t testing.TB) string {
8891
func (r *crdbTester) NewDatastore(t testing.TB, initFunc InitFunc) datastore.Datastore {
8992
connectStr := r.NewDatabase(t)
9093

91-
migrationDriver, err := crdbmigrations.NewCRDBDriver(connectStr)
94+
migrationDriver, err := crdbmigrations.NewCRDBDriver(t.Context(), connectStr)
9295
require.NoError(t, err)
9396
require.NoError(t, crdbmigrations.CRDBMigrations.Run(t.Context(), migrationDriver, migrate.Head, migrate.LiveRun))
9497
t.Cleanup(func() {

0 commit comments

Comments
 (0)