Skip to content

Commit 658cecb

Browse files
craig[bot]petermattis
andcommitted
Merge #150562
150562: sql/stats: prevent stats collection in read-only tenants r=petermattis a=petermattis This change prevents both automatic and manual statistics collection in PCR reader tenants by detecting read-only tenant status at the tenant level rather than checking individual table descriptors. Key changes: - Add TenantReadonly field to SQLConfig and ExecutorConfig to track tenant-level read-only status - Modify tenant initialization to detect read-only tenants by checking mtinfopb.TenantInfoWithUsage.ReadFromTenant \!= nil - Update MakeRefresher to accept readOnlyTenant parameter - Prevent automatic stats collection in autoStatsEnabled() - Prevent manual stats collection in makeJobRecord() - Add TenantReadonly field to TestSharedProcessTenantArgs for testing - Update tests to use tenant-level detection instead of table-level ReplicatedPCRVersion checks - Enhance test coverage for both autoStatsEnabled() and autoStatsEnabledForTableID() functions The tenant-level approach is more reliable than checking individual table descriptors because some tables within read-only tenants are not replicated (e.g. system.statement_statistics), yet we cannot record stats for any tables since the system.table_statistics table itself is read-only. Fixes #135828 Release note: None 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> Co-authored-by: Peter Mattis <[email protected]>
2 parents 73f993e + ab73c65 commit 658cecb

File tree

9 files changed

+191
-25
lines changed

9 files changed

+191
-25
lines changed

pkg/base/test_server_args.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,10 @@ type TestSharedProcessTenantArgs struct {
635635
// TenantID is the ID of the tenant to be created. If not set, an ID is
636636
// assigned automatically.
637637
TenantID roachpb.TenantID
638+
// TenantReadOnly indicates if this tenant should be created as read-only
639+
// (for testing PCR reader tenants). This field is used for testing purposes
640+
// and overrides the tenant record check.
641+
TenantReadOnly bool
638642

639643
Knobs TestingKnobs
640644

pkg/server/config.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,9 @@ type SQLConfig struct {
487487
TenantID roachpb.TenantID
488488
TenantName roachpb.TenantName
489489

490+
// TenantReadOnly indicates if this tenant is read-only (PCR reader tenant).
491+
TenantReadOnly bool
492+
490493
// If set, will to be called at server startup to obtain the tenant id and
491494
// locality.
492495
DelayedSetTenantID func(context.Context) (roachpb.TenantID, roachpb.Locality, error)
@@ -554,8 +557,9 @@ func MakeSQLConfig(
554557
tenID roachpb.TenantID, tenName roachpb.TenantName, tempStorageCfg base.TempStorageConfig,
555558
) SQLConfig {
556559
sqlCfg := SQLConfig{
557-
TenantID: tenID,
558-
TenantName: tenName,
560+
TenantID: tenID,
561+
TenantName: tenName,
562+
TenantReadOnly: false, // Default to false, will be set during tenant initialization
559563
}
560564
sqlCfg.SetDefaults(tempStorageCfg)
561565
return sqlCfg

pkg/server/server_controller_new_server.go

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,18 @@ func (s *topLevelServer) newTenantServer(
6464
portStartHint int,
6565
testArgs base.TestSharedProcessTenantArgs,
6666
) (onDemandServer, error) {
67-
tenantID, err := s.getTenantID(ctx, tenantNameContainer.Get())
67+
tenantID, tenantReadOnly, err := s.getTenantID(ctx, tenantNameContainer.Get())
6868
if err != nil {
6969
return nil, err
7070
}
7171

72+
// Use test override for tenant read-only status if provided.
73+
if testArgs.TenantReadOnly {
74+
tenantReadOnly = true
75+
}
76+
7277
baseCfg, sqlCfg, err := s.makeSharedProcessTenantConfig(ctx, tenantID, tenantNameContainer.Get(), portStartHint,
73-
tenantStopper, testArgs.Settings)
78+
tenantStopper, testArgs.Settings, tenantReadOnly)
7479
if err != nil {
7580
return nil, err
7681
}
@@ -100,23 +105,27 @@ var ErrInvalidTenant error = errInvalidTenantMarker{}
100105

101106
func (s *topLevelServer) getTenantID(
102107
ctx context.Context, tenantName roachpb.TenantName,
103-
) (roachpb.TenantID, error) {
108+
) (roachpb.TenantID, bool, error) {
104109
var rec *mtinfopb.TenantInfo
105110
if err := s.sqlServer.internalDB.Txn(ctx, func(ctx context.Context, txn isql.Txn) error {
106111
var err error
107112
rec, err = sql.GetTenantRecordByName(ctx, s.cfg.Settings, txn, tenantName)
108113
return err
109114
}); err != nil {
110-
return roachpb.TenantID{}, errors.Mark(err, ErrInvalidTenant)
115+
return roachpb.TenantID{}, false, errors.Mark(err, ErrInvalidTenant)
111116
}
112117

113118
tenantID, err := roachpb.MakeTenantID(rec.ID)
114119
if err != nil {
115-
return roachpb.TenantID{}, errors.Mark(
120+
return roachpb.TenantID{}, false, errors.Mark(
116121
errors.NewAssertionErrorWithWrappedErrf(err, "stored tenant ID %d does not convert to TenantID", rec.ID),
117122
ErrInvalidTenant)
118123
}
119-
return tenantID, nil
124+
125+
// Check if tenant is read-only (PCR reader tenant).
126+
readOnlyTenant := rec.ReadFromTenant != nil
127+
128+
return tenantID, readOnlyTenant, nil
120129
}
121130

122131
// newTenantServerInternal instantiates a server for the given target
@@ -151,6 +160,7 @@ func (s *topLevelServer) makeSharedProcessTenantConfig(
151160
portStartHint int,
152161
stopper *stop.Stopper,
153162
testSettings *cluster.Settings,
163+
tenantReadOnly bool,
154164
) (BaseConfig, SQLConfig, error) {
155165
// Create a configuration for the new tenant.
156166
parentCfg := s.cfg
@@ -167,7 +177,7 @@ func (s *topLevelServer) makeSharedProcessTenantConfig(
167177
}
168178

169179
baseCfg, sqlCfg, err := makeSharedProcessTenantServerConfig(ctx, tenantID, tenantName, portStartHint, parentCfg,
170-
localServerInfo, st, stopper, s.recorder)
180+
localServerInfo, st, stopper, s.recorder, tenantReadOnly)
171181
if err != nil {
172182
return BaseConfig{}, SQLConfig{}, err
173183
}
@@ -186,6 +196,7 @@ func makeSharedProcessTenantServerConfig(
186196
st *cluster.Settings,
187197
stopper *stop.Stopper,
188198
nodeMetricsRecorder *status.MetricsRecorder,
199+
tenantReadOnly bool,
189200
) (baseCfg BaseConfig, sqlCfg SQLConfig, err error) {
190201
tr := tracing.NewTracerWithOpt(ctx, tracing.WithClusterSettings(&st.SV))
191202

@@ -333,6 +344,7 @@ func makeSharedProcessTenantServerConfig(
333344
}
334345

335346
sqlCfg = MakeSQLConfig(tenantID, tenantName, tempStorageCfg)
347+
sqlCfg.TenantReadOnly = tenantReadOnly
336348
baseCfg.ExternalIODirConfig = kvServerCfg.BaseConfig.ExternalIODirConfig
337349

338350
baseCfg.ExternalIODir = kvServerCfg.BaseConfig.ExternalIODir

pkg/server/server_sql.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,6 +1055,7 @@ func newSQLServer(ctx context.Context, cfg sqlServerArgs) (*SQLServer, error) {
10551055
RangeStatsFetcher: rangeStatsFetcher,
10561056
NodeDescs: cfg.nodeDescs,
10571057
TenantCapabilitiesReader: cfg.tenantCapabilitiesReader,
1058+
TenantReadOnly: cfg.SQLConfig.TenantReadOnly,
10581059
CidrLookup: cfg.BaseConfig.CidrLookup,
10591060
LicenseEnforcer: cfg.SQLConfig.LicenseEnforcer,
10601061
}
@@ -1203,6 +1204,7 @@ func newSQLServer(ctx context.Context, cfg sqlServerArgs) (*SQLServer, error) {
12031204
execCfg.TableStatsCache,
12041205
stats.DefaultAsOfTime,
12051206
tableStatsTestingKnobs,
1207+
execCfg.TenantReadOnly,
12061208
)
12071209
execCfg.StatsRefresher = statsRefresher
12081210
distSQLServer.ServerConfig.StatsRefresher = statsRefresher

pkg/sql/catalog/replication/reader_catalog_test.go

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,11 @@ func newReaderCatalogTest(
7070
})
7171
require.NoError(t, err)
7272
destTenant, _, err := ts.StartSharedProcessTenant(ctx, base.TestSharedProcessTenantArgs{
73-
TenantID: serverutils.TestTenantID2(),
74-
TenantName: "dest",
75-
Knobs: destTestingKnobs,
76-
Settings: destSettings,
73+
TenantID: serverutils.TestTenantID2(),
74+
TenantName: "dest",
75+
Knobs: destTestingKnobs,
76+
Settings: destSettings,
77+
TenantReadOnly: true, // Mark the dest tenant as read-only for testing
7778
})
7879
require.NoError(t, err)
7980
srcRunner := sqlutils.MakeSQLRunner(srcTenant.SQLConn(t))
@@ -550,6 +551,34 @@ func TestReaderCatalogTSAdvanceWithLongTxn(t *testing.T) {
550551
require.NoError(t, tx.Commit())
551552
}
552553

554+
func TestReaderCatalogAutoStatsDisabled(t *testing.T) {
555+
defer leaktest.AfterTest(t)()
556+
skip.UnderDuress(t)
557+
558+
ctx := context.Background()
559+
r, cleanup := newReaderCatalogTest(t, ctx, base.TestingKnobs{}, nil)
560+
defer cleanup()
561+
562+
// Create a table and insert some data in the source tenant.
563+
r.srcRunner.Exec(t, `
564+
CREATE TABLE t1(n int);
565+
INSERT INTO t1 VALUES (1), (2), (3);
566+
`)
567+
568+
// Enable auto stats collection in the source tenant.
569+
r.srcRunner.Exec(t, `SET CLUSTER SETTING sql.stats.automatic_collection.enabled = true`)
570+
571+
// Advance the reader catalog timestamp to replicate the table.
572+
require.NoError(t, r.advanceTS(ctx, r.ts.Clock().Now(), true))
573+
574+
// Verify the table exists in the reader catalog.
575+
r.compareEqual(t, "SELECT * FROM t1 ORDER BY n")
576+
577+
// Now verify that stats collection is disabled for the read-only tenant.
578+
// Manual CREATE STATISTICS should fail with our tenant-level read-only error.
579+
r.destRunner.ExpectErr(t, "cannot create statistics in read-only tenant", "CREATE STATISTICS test_stats FROM t1")
580+
}
581+
553582
func TestMain(m *testing.M) {
554583
securityassets.SetLoader(securitytest.EmbeddedAssets)
555584
randutil.SeedForTests()

pkg/sql/create_stats.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,12 @@ func (n *createStatsNode) runJob(ctx context.Context) error {
229229
// makeJobRecord creates a CreateStats job record which can be used to plan and
230230
// execute statistics creation.
231231
func (n *createStatsNode) makeJobRecord(ctx context.Context) (*jobs.Record, error) {
232+
// Check tenant-level read-only status first (applies to all tables in tenant).
233+
if n.p.ExecCfg().TenantReadOnly {
234+
return nil, pgerror.Newf(
235+
pgcode.WrongObjectType, "cannot create statistics in read-only tenant")
236+
}
237+
232238
var tableDesc catalog.TableDescriptor
233239
var fqTableName string
234240
var err error

pkg/sql/exec_util.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1678,6 +1678,9 @@ type ExecutorConfig struct {
16781678

16791679
TenantCapabilitiesReader SystemTenantOnly[tenantcapabilities.Reader]
16801680

1681+
// TenantReadOnly indicates if this tenant is read-only (PCR reader tenant).
1682+
TenantReadOnly bool
1683+
16811684
// VirtualClusterName contains the name of the virtual cluster
16821685
// (tenant).
16831686
VirtualClusterName roachpb.TenantName

pkg/sql/stats/automatic_stats.go

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -267,11 +267,12 @@ const (
267267
// sent.
268268
type Refresher struct {
269269
log.AmbientContext
270-
st *cluster.Settings
271-
internalDB descs.DB
272-
cache *TableStatisticsCache
273-
randGen autoStatsRand
274-
knobs *TableStatsTestingKnobs
270+
st *cluster.Settings
271+
internalDB descs.DB
272+
cache *TableStatisticsCache
273+
randGen autoStatsRand
274+
knobs *TableStatsTestingKnobs
275+
readOnlyTenant bool
275276

276277
// mutations is the buffered channel used to pass messages containing
277278
// metadata about SQL mutations to the background Refresher thread.
@@ -353,6 +354,7 @@ func MakeRefresher(
353354
cache *TableStatisticsCache,
354355
asOfTime time.Duration,
355356
knobs *TableStatsTestingKnobs,
357+
readOnlyTenant bool,
356358
) *Refresher {
357359
randSource := rand.NewSource(rand.Int63())
358360

@@ -363,6 +365,7 @@ func MakeRefresher(
363365
cache: cache,
364366
randGen: makeAutoStatsRand(randSource),
365367
knobs: knobs,
368+
readOnlyTenant: readOnlyTenant,
366369
mutations: make(chan mutation, refreshChanBufferLen),
367370
settings: make(chan settingOverride, refreshChanBufferLen),
368371
asOfTime: asOfTime,
@@ -378,6 +381,11 @@ func (r *Refresher) getNumTablesEnsured() int {
378381
}
379382

380383
func (r *Refresher) autoStatsEnabled(desc catalog.TableDescriptor) bool {
384+
// Check tenant-level read-only status first (applies to all tables in tenant).
385+
if r.readOnlyTenant {
386+
return false
387+
}
388+
381389
if desc == nil {
382390
// If the descriptor could not be accessed, defer to the cluster setting.
383391
return AutomaticStatisticsClusterMode.Get(&r.st.SV)
@@ -432,6 +440,11 @@ func (r *Refresher) autoFullStatsEnabled(desc catalog.TableDescriptor) bool {
432440
func (r *Refresher) autoStatsEnabledForTableID(
433441
tableID descpb.ID, settingOverrides map[descpb.ID]catpb.AutoStatsSettings,
434442
) bool {
443+
// Check tenant-level read-only status first (applies to all tables in tenant).
444+
if r.readOnlyTenant {
445+
return false
446+
}
447+
435448
var setting catpb.AutoStatsSettings
436449
var ok bool
437450
if settingOverrides == nil {
@@ -533,6 +546,12 @@ func (r *Refresher) SetDraining() {
533546
func (r *Refresher) Start(
534547
ctx context.Context, stopper *stop.Stopper, refreshInterval time.Duration,
535548
) error {
549+
// If the tenant is read-only, we don't need to start the stats refresher
550+
// goroutines as we can't persist collected stats.
551+
if r.readOnlyTenant {
552+
return nil
553+
}
554+
536555
// The refresher has the same lifetime as the server, so the cancellation
537556
// function can be ignored and it'll be called by the stopper.
538557
stoppingCtx, _ := stopper.WithCancelOnQuiesce(context.Background()) // nolint:quiesce

0 commit comments

Comments
 (0)