Skip to content

Commit 6b43353

Browse files
committed
feat: centralize migration error handling with gRPC readiness middleware
Replaces scattered IsMissingTableError checks (15 files, 157 lines) with a single gRPC readiness middleware that blocks ALL requests until the datastore is migrated. Key improvements: - Single point of control vs copy-pasted checks everywhere - Impossible to miss code paths - all gRPC requests gated automatically - Clear error message: "Please run 'spicedb datastore migrate'" - Cached checks (500ms) with singleflight to prevent thundering herd - Health probes bypass the gate for Kubernetes compatibility Net: -81 lines, better coverage, consistent UX.
1 parent 5b11989 commit 6b43353

23 files changed

Lines changed: 640 additions & 145 deletions

internal/datastore/crdb/caveat.go

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@ import (
99
sq "github.com/Masterminds/squirrel"
1010
"github.com/jackc/pgx/v5"
1111

12-
dscommon "github.com/authzed/spicedb/internal/datastore/common"
1312
"github.com/authzed/spicedb/internal/datastore/crdb/schema"
14-
pgxcommon "github.com/authzed/spicedb/internal/datastore/postgres/common"
1513
"github.com/authzed/spicedb/internal/datastore/revisions"
1614
"github.com/authzed/spicedb/pkg/datastore"
1715
core "github.com/authzed/spicedb/pkg/proto/core/v1"
@@ -55,9 +53,6 @@ func (cr *crdbReader) LegacyReadCaveatByName(ctx context.Context, name string) (
5553
if errors.Is(err, pgx.ErrNoRows) {
5654
err = datastore.NewCaveatNameNotFoundErr(name)
5755
}
58-
if pgxcommon.IsMissingTableError(err) {
59-
err = dscommon.NewSchemaNotInitializedError(err)
60-
}
6156
return nil, datastore.NoRevision, fmt.Errorf(errReadCaveat, name, err)
6257
}
6358

@@ -114,9 +109,6 @@ func (cr *crdbReader) lookupCaveats(ctx context.Context, caveatNames []string) (
114109
return nil
115110
}, sql, args...)
116111
if err != nil {
117-
if pgxcommon.IsMissingTableError(err) {
118-
err = dscommon.NewSchemaNotInitializedError(err)
119-
}
120112
return nil, fmt.Errorf(errListCaveats, err)
121113
}
122114

internal/datastore/crdb/crdb.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -592,11 +592,7 @@ func (cds *crdbDatastore) features(ctx context.Context) (*datastore.Features, er
592592
}
593593

594594
features.Watch.Status = datastore.FeatureUnsupported
595-
if pgxcommon.IsMissingTableError(err) {
596-
features.Watch.Reason = "Database schema has not been initialized. Please run \"spicedb datastore migrate\": " + err.Error()
597-
} else {
598-
features.Watch.Reason = "Range feeds must be enabled in CockroachDB and the user must have permission to create them in order to enable the Watch API: " + err.Error()
599-
}
595+
features.Watch.Reason = "Range feeds must be enabled in CockroachDB and the user must have permission to create them in order to enable the Watch API: " + err.Error()
600596
return nil
601597
}, fmt.Sprintf(cds.beginChangefeedQuery, cds.schema.RelationshipTableName, head, "-1s"))
602598
} else {

internal/datastore/crdb/reader.go

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,6 @@ func (cr *crdbReader) CountRelationships(ctx context.Context, name string) (int,
129129
return row.Scan(&count)
130130
}, sql, args...)
131131
if err != nil {
132-
if wrappedErr := pgxcommon.WrapMissingTableError(err); wrappedErr != nil {
133-
return 0, wrappedErr
134-
}
135132
return 0, err
136133
}
137134

@@ -196,9 +193,6 @@ func (cr *crdbReader) lookupCounters(ctx context.Context, optionalFilterName str
196193
return nil
197194
}, sql, args...)
198195
if err != nil {
199-
if wrappedErr := pgxcommon.WrapMissingTableError(err); wrappedErr != nil {
200-
return nil, wrappedErr
201-
}
202196
return nil, err
203197
}
204198

@@ -214,9 +208,6 @@ func (cr *crdbReader) LegacyReadNamespaceByName(
214208
if errors.As(err, &datastore.NamespaceNotFoundError{}) {
215209
return nil, datastore.NoRevision, err
216210
}
217-
if wrappedErr := pgxcommon.WrapMissingTableError(err); wrappedErr != nil {
218-
return nil, datastore.NoRevision, wrappedErr
219-
}
220211
return nil, datastore.NoRevision, fmt.Errorf(errUnableToReadConfig, err)
221212
}
222213

@@ -230,9 +221,6 @@ func (cr *crdbReader) LegacyListAllNamespaces(ctx context.Context) ([]datastore.
230221

231222
nsDefs, sql, err := loadAllNamespaces(ctx, cr.query, addFromToQuery)
232223
if err != nil {
233-
if wrappedErr := pgxcommon.WrapMissingTableError(err); wrappedErr != nil {
234-
return nil, wrappedErr
235-
}
236224
return nil, fmt.Errorf(errUnableToListNamespaces, err)
237225
}
238226
cr.assertHasExpectedAsOfSystemTime(sql)
@@ -245,9 +233,6 @@ func (cr *crdbReader) LegacyLookupNamespacesWithNames(ctx context.Context, nsNam
245233
}
246234
nsDefs, err := cr.lookupNamespaces(ctx, cr.query, nsNames)
247235
if err != nil {
248-
if wrappedErr := pgxcommon.WrapMissingTableError(err); wrappedErr != nil {
249-
return nil, wrappedErr
250-
}
251236
return nil, fmt.Errorf(errUnableToListNamespaces, err)
252237
}
253238
return nsDefs, nil

internal/datastore/crdb/stats.go

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,6 @@ func (cds *crdbDatastore) UniqueID(ctx context.Context) (string, error) {
3333
if err := cds.readPool.QueryRowFunc(ctx, func(ctx context.Context, row pgx.Row) error {
3434
return row.Scan(&uniqueID)
3535
}, sql, args...); err != nil {
36-
if wrappedErr := pgxcommon.WrapMissingTableError(err); wrappedErr != nil {
37-
return "", wrappedErr
38-
}
3936
return "", fmt.Errorf("unable to query unique ID: %w", err)
4037
}
4138

@@ -62,9 +59,6 @@ func (cds *crdbDatastore) Statistics(ctx context.Context) (datastore.Stats, erro
6259
return sb.From(tableName)
6360
})
6461
if err != nil {
65-
if wrappedErr := pgxcommon.WrapMissingTableError(err); wrappedErr != nil {
66-
return wrappedErr
67-
}
6862
return fmt.Errorf("unable to read namespaces: %w", err)
6963
}
7064
return nil
@@ -75,9 +69,6 @@ func (cds *crdbDatastore) Statistics(ctx context.Context) (datastore.Stats, erro
7569
if cds.analyzeBeforeStatistics {
7670
if err := cds.readPool.BeginTxFunc(ctx, pgx.TxOptions{AccessMode: pgx.ReadOnly}, func(tx pgx.Tx) error {
7771
if _, err := tx.Exec(ctx, "ANALYZE "+cds.schema.RelationshipTableName); err != nil {
78-
if wrappedErr := pgxcommon.WrapMissingTableError(err); wrappedErr != nil {
79-
return wrappedErr
80-
}
8172
return fmt.Errorf("unable to analyze tuple table: %w", err)
8273
}
8374

@@ -152,9 +143,6 @@ func (cds *crdbDatastore) Statistics(ctx context.Context) (datastore.Stats, erro
152143
log.Warn().Bool("has-rows", hasRows).Msg("unable to find row count in statistics query result")
153144
return nil
154145
}, "SHOW STATISTICS FOR TABLE "+cds.schema.RelationshipTableName); err != nil {
155-
if wrappedErr := pgxcommon.WrapMissingTableError(err); wrappedErr != nil {
156-
return datastore.Stats{}, wrappedErr
157-
}
158146
return datastore.Stats{}, fmt.Errorf("unable to query unique estimated row count: %w", err)
159147
}
160148

internal/datastore/mysql/caveat.go

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
sq "github.com/Masterminds/squirrel"
1010

1111
"github.com/authzed/spicedb/internal/datastore/common"
12-
mysqlcommon "github.com/authzed/spicedb/internal/datastore/mysql/common"
1312
"github.com/authzed/spicedb/internal/datastore/revisions"
1413
"github.com/authzed/spicedb/pkg/datastore"
1514
core "github.com/authzed/spicedb/pkg/proto/core/v1"
@@ -42,9 +41,6 @@ func (mr *mysqlReader) LegacyReadCaveatByName(ctx context.Context, name string)
4241
if errors.Is(err, sql.ErrNoRows) {
4342
return nil, datastore.NoRevision, datastore.NewCaveatNameNotFoundErr(name)
4443
}
45-
if wrappedErr := mysqlcommon.WrapMissingTableError(err); wrappedErr != nil {
46-
return nil, datastore.NoRevision, wrappedErr
47-
}
4844
return nil, datastore.NoRevision, fmt.Errorf(errReadCaveat, err)
4945
}
5046
def := core.CaveatDefinition{}
@@ -86,9 +82,6 @@ func (mr *mysqlReader) lookupCaveats(ctx context.Context, caveatNames []string)
8682

8783
rows, err := tx.QueryContext(ctx, listSQL, listArgs...)
8884
if err != nil {
89-
if wrappedErr := mysqlcommon.WrapMissingTableError(err); wrappedErr != nil {
90-
return nil, wrappedErr
91-
}
9285
return nil, fmt.Errorf(errListCaveats, err)
9386
}
9487
defer common.LogOnError(ctx, rows.Close)

internal/datastore/mysql/reader.go

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
sq "github.com/Masterminds/squirrel"
1010

1111
"github.com/authzed/spicedb/internal/datastore/common"
12-
mysqlcommon "github.com/authzed/spicedb/internal/datastore/mysql/common"
1312
"github.com/authzed/spicedb/internal/datastore/revisions"
1413
schemautil "github.com/authzed/spicedb/internal/datastore/schema"
1514
"github.com/authzed/spicedb/pkg/datastore"
@@ -77,9 +76,6 @@ func (mr *mysqlReader) CountRelationships(ctx context.Context, name string) (int
7776
var count int
7877
rows, err := tx.QueryContext(ctx, sql, args...)
7978
if err != nil {
80-
if wrappedErr := mysqlcommon.WrapMissingTableError(err); wrappedErr != nil {
81-
return 0, wrappedErr
82-
}
8379
return 0, err
8480
}
8581
defer common.LogOnError(ctx, rows.Close)
@@ -127,9 +123,6 @@ func (mr *mysqlReader) lookupCounters(ctx context.Context, optionalName string)
127123

128124
rows, err := tx.QueryContext(ctx, sql, args...)
129125
if err != nil {
130-
if wrappedErr := mysqlcommon.WrapMissingTableError(err); wrappedErr != nil {
131-
return nil, wrappedErr
132-
}
133126
return nil, err
134127
}
135128
defer common.LogOnError(ctx, rows.Close)
@@ -230,9 +223,6 @@ func (mr *mysqlReader) LegacyReadNamespaceByName(ctx context.Context, nsName str
230223
case err == nil:
231224
return loaded, version, nil
232225
default:
233-
if wrappedErr := mysqlcommon.WrapMissingTableError(err); wrappedErr != nil {
234-
return nil, datastore.NoRevision, wrappedErr
235-
}
236226
return nil, datastore.NoRevision, fmt.Errorf(errUnableToReadConfig, err)
237227
}
238228
}
@@ -275,9 +265,6 @@ func (mr *mysqlReader) LegacyListAllNamespaces(ctx context.Context) ([]datastore
275265

276266
nsDefs, err := loadAllNamespaces(ctx, tx, query)
277267
if err != nil {
278-
if wrappedErr := mysqlcommon.WrapMissingTableError(err); wrappedErr != nil {
279-
return nil, wrappedErr
280-
}
281268
return nil, fmt.Errorf(errUnableToListNamespaces, err)
282269
}
283270

@@ -304,9 +291,6 @@ func (mr *mysqlReader) LegacyLookupNamespacesWithNames(ctx context.Context, nsNa
304291

305292
nsDefs, err := loadAllNamespaces(ctx, tx, query)
306293
if err != nil {
307-
if wrappedErr := mysqlcommon.WrapMissingTableError(err); wrappedErr != nil {
308-
return nil, wrappedErr
309-
}
310294
return nil, fmt.Errorf(errUnableToListNamespaces, err)
311295
}
312296

internal/datastore/mysql/stats.go

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"github.com/ccoveille/go-safecast/v2"
1010

1111
"github.com/authzed/spicedb/internal/datastore/common"
12-
mysqlcommon "github.com/authzed/spicedb/internal/datastore/mysql/common"
1312
"github.com/authzed/spicedb/pkg/datastore"
1413
"github.com/authzed/spicedb/pkg/spiceerrors"
1514
)
@@ -27,9 +26,6 @@ func (mds *Datastore) Statistics(ctx context.Context) (datastore.Stats, error) {
2726
if mds.analyzeBeforeStats {
2827
_, err := mds.db.ExecContext(ctx, "ANALYZE TABLE "+mds.driver.RelationTuple())
2928
if err != nil {
30-
if wrappedErr := mysqlcommon.WrapMissingTableError(err); wrappedErr != nil {
31-
return datastore.Stats{}, wrappedErr
32-
}
3329
return datastore.Stats{}, fmt.Errorf("unable to run ANALYZE TABLE: %w", err)
3430
}
3531
}
@@ -51,9 +47,6 @@ func (mds *Datastore) Statistics(ctx context.Context) (datastore.Stats, error) {
5147
var count sql.NullInt64
5248
err = mds.db.QueryRowContext(ctx, query, args...).Scan(&count)
5349
if err != nil {
54-
if wrappedErr := mysqlcommon.WrapMissingTableError(err); wrappedErr != nil {
55-
return datastore.Stats{}, wrappedErr
56-
}
5750
return datastore.Stats{}, err
5851
}
5952

@@ -66,9 +59,6 @@ func (mds *Datastore) Statistics(ctx context.Context) (datastore.Stats, error) {
6659
}
6760
err = mds.db.QueryRowContext(ctx, query, args...).Scan(&count)
6861
if err != nil {
69-
if wrappedErr := mysqlcommon.WrapMissingTableError(err); wrappedErr != nil {
70-
return datastore.Stats{}, wrappedErr
71-
}
7262
return datastore.Stats{}, err
7363
}
7464
}
@@ -83,9 +73,6 @@ func (mds *Datastore) Statistics(ctx context.Context) (datastore.Stats, error) {
8373

8474
nsDefs, err := loadAllNamespaces(ctx, tx, nsQuery)
8575
if err != nil {
86-
if wrappedErr := mysqlcommon.WrapMissingTableError(err); wrappedErr != nil {
87-
return datastore.Stats{}, wrappedErr
88-
}
8976
return datastore.Stats{}, fmt.Errorf("unable to load namespaces: %w", err)
9077
}
9178

@@ -110,9 +97,6 @@ func (mds *Datastore) UniqueID(ctx context.Context) (string, error) {
11097

11198
var uniqueID string
11299
if err := mds.db.QueryRowContext(ctx, sql, args...).Scan(&uniqueID); err != nil {
113-
if wrappedErr := mysqlcommon.WrapMissingTableError(err); wrappedErr != nil {
114-
return "", wrappedErr
115-
}
116100
return "", fmt.Errorf("unable to query unique ID: %w", err)
117101
}
118102
mds.uniqueID.Store(&uniqueID)

internal/datastore/postgres/caveat.go

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
sq "github.com/Masterminds/squirrel"
99
"github.com/jackc/pgx/v5"
1010

11-
pgxcommon "github.com/authzed/spicedb/internal/datastore/postgres/common"
1211
"github.com/authzed/spicedb/internal/datastore/postgres/schema"
1312
"github.com/authzed/spicedb/pkg/datastore"
1413
"github.com/authzed/spicedb/pkg/genutil/mapz"
@@ -50,9 +49,6 @@ func (r *pgReader) LegacyReadCaveatByName(ctx context.Context, name string) (*co
5049
if errors.Is(err, pgx.ErrNoRows) {
5150
return nil, datastore.NoRevision, datastore.NewCaveatNameNotFoundErr(name)
5251
}
53-
if wrappedErr := pgxcommon.WrapMissingTableError(err); wrappedErr != nil {
54-
return nil, datastore.NoRevision, wrappedErr
55-
}
5652
return nil, datastore.NoRevision, fmt.Errorf(errReadCaveat, err)
5753
}
5854
def := core.CaveatDefinition{}
@@ -110,9 +106,6 @@ func (r *pgReader) lookupCaveats(ctx context.Context, caveatNames []string) ([]d
110106
return rows.Err()
111107
}, sql, args...)
112108
if err != nil {
113-
if wrappedErr := pgxcommon.WrapMissingTableError(err); wrappedErr != nil {
114-
return nil, wrappedErr
115-
}
116109
return nil, fmt.Errorf(errListCaveats, err)
117110
}
118111

internal/datastore/postgres/common/bulk.go

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"github.com/ccoveille/go-safecast/v2"
77
"github.com/jackc/pgx/v5"
88

9-
dscommon "github.com/authzed/spicedb/internal/datastore/common"
109
"github.com/authzed/spicedb/pkg/datastore"
1110
"github.com/authzed/spicedb/pkg/spiceerrors"
1211
"github.com/authzed/spicedb/pkg/tuple"
@@ -78,15 +77,9 @@ func BulkLoad(
7877
colNames: colNames,
7978
}
8079
copied, err := tx.CopyFrom(ctx, pgx.Identifier{tupleTableName}, colNames, adapter)
81-
if err != nil {
82-
if IsMissingTableError(err) {
83-
return 0, dscommon.NewSchemaNotInitializedError(err)
84-
}
85-
return 0, err
86-
}
8780
uintCopied, castErr := safecast.Convert[uint64](copied)
8881
if castErr != nil {
8982
return 0, spiceerrors.MustBugf("number copied was negative: %v", castErr)
9083
}
91-
return uintCopied, nil
84+
return uintCopied, err
9285
}

internal/datastore/postgres/reader.go

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,6 @@ func (r *pgReader) CountRelationships(ctx context.Context, name string) (int, er
8585
return rows.Err()
8686
}, sql, args...)
8787
if err != nil {
88-
if pgxcommon.IsMissingTableError(err) {
89-
err = common.NewSchemaNotInitializedError(err)
90-
}
9188
return 0, err
9289
}
9390

@@ -143,9 +140,6 @@ func (r *pgReader) lookupCounters(ctx context.Context, optionalName string) ([]d
143140
return rows.Err()
144141
}, sql, args...)
145142
if err != nil {
146-
if pgxcommon.IsMissingTableError(err) {
147-
err = common.NewSchemaNotInitializedError(err)
148-
}
149143
return nil, fmt.Errorf("unable to query counters: %w", err)
150144
}
151145

@@ -214,9 +208,6 @@ func (r *pgReader) LegacyReadNamespaceByName(ctx context.Context, nsName string)
214208
case err == nil:
215209
return loaded, version, nil
216210
default:
217-
if pgxcommon.IsMissingTableError(err) {
218-
err = common.NewSchemaNotInitializedError(err)
219-
}
220211
return nil, datastore.NoRevision, fmt.Errorf(errUnableToReadConfig, err)
221212
}
222213
}
@@ -242,9 +233,6 @@ func (r *pgReader) loadNamespace(ctx context.Context, namespace string, tx pgxco
242233
func (r *pgReader) LegacyListAllNamespaces(ctx context.Context) ([]datastore.RevisionedNamespace, error) {
243234
nsDefsWithRevisions, err := loadAllNamespaces(ctx, r.query, r.aliveFilter)
244235
if err != nil {
245-
if pgxcommon.IsMissingTableError(err) {
246-
err = common.NewSchemaNotInitializedError(err)
247-
}
248236
return nil, fmt.Errorf(errUnableToListNamespaces, err)
249237
}
250238

@@ -265,9 +253,6 @@ func (r *pgReader) LegacyLookupNamespacesWithNames(ctx context.Context, nsNames
265253
return r.aliveFilter(original).Where(clause)
266254
})
267255
if err != nil {
268-
if pgxcommon.IsMissingTableError(err) {
269-
err = common.NewSchemaNotInitializedError(err)
270-
}
271256
return nil, fmt.Errorf(errUnableToListNamespaces, err)
272257
}
273258

0 commit comments

Comments
 (0)