-
Notifications
You must be signed in to change notification settings - Fork 777
statistics, schedule: stop republishing metrics for a known-tombstoned store #11166
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 2 commits
892b10f
d1b7f82
d5edd6c
4a04da5
dd84e1b
c45456f
65b0127
89d1b6a
42faef5
ef71a1d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1790,6 +1790,7 @@ func (c *RaftCluster) BuryStoreLocked(storeID uint64, forceBury bool) error { | |
| // clean up the residual information. | ||
| c.prevStoreLimit.Delete(storeID) | ||
| c.RemoveStoreLimit(storeID) | ||
| c.ruleManager.RemoveStoreCache(storeID) | ||
| storeIDStr := strconv.FormatUint(storeID, 10) | ||
| statistics.ResetStoreStatistics(storeIDStr) | ||
| filter.DeleteStoreMetrics(storeIDStr) | ||
|
|
@@ -2182,7 +2183,6 @@ func (c *RaftCluster) RemoveTombStoneRecords() error { | |
| errs.ZapError(err)) | ||
| return err | ||
| } | ||
| c.RemoveStoreLimit(store.GetID()) | ||
| log.Info("delete store succeeded", | ||
| zap.Stringer("store", store.GetMeta())) | ||
| } | ||
|
|
@@ -2212,6 +2212,13 @@ func (c *RaftCluster) deleteStore(store *core.StoreInfo) error { | |
| // leaving a series this cleanup just deleted with no later event able to | ||
| // find and remove it again. | ||
| c.DeleteStore(store) | ||
| c.ruleManager.RemoveStoreCache(store.GetID()) | ||
| // The auto-GC path (checkStores' NodeState_Removed branch) only ever calls | ||
| // deleteStore, never RemoveTombStoneRecords, so the store-limit config entry | ||
| // needs to be cleared here rather than by the manual remove-tombstone caller | ||
| // alone -- otherwise a store reclaimed purely by the 30-day auto-GC timer | ||
| // never gets this cleanup at all. | ||
| c.RemoveStoreLimit(store.GetID()) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add a unit test for the exhausted-persistence path. It must verify durable state, not only the in-memory schedule config: func TestDeleteStoreDoesNotLoseStoreLimitCleanup(t *testing.T) {
re := require.New(t)
// Create and persist a store-limit entry before injecting the failure.
// Use a tombstone store and the same storage instance for the cluster and reload.
re.NoError(cluster.SetStoreLimit(storeID, storelimit.AddPeer, 60))
const persistFail = "github.com/tikv/pd/server/config/persistFail"
re.NoError(failpoint.Enable(persistFail, "return(true)"))
err := cluster.deleteStore(tombstone) // all five cleanup attempts fail
re.NoError(failpoint.Disable(persistFail))
// The deletion must report the cleanup failure or enqueue a retry.
re.Error(t, err)
// After the retry path runs, reload the config from storage and verify
// that StoreLimit no longer contains storeID.
}The test should force every in-method retry to fail, then clear the failure and exercise the retry path added by the fix. The final assertion must reload from durable storage. The current implementation returns success and has no later path that retries RemoveStoreLimit, so the stale entry remains after restart.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Confirmed but not adding the requested test: it would demonstrate an accepted gap rather than a behavior change. Tracked in the PR description's Known limitations -- closing it needs either a startup reconciliation between StoresInfo and the persisted StoreLimit config, or making RemoveStoreLimit's failure retryable/observable, both out of scope here given how narrow the trigger is.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Blocking: this new call adds another full-config writer to the auto-GC path, but it can lose a concurrent A deterministic interleaving is:
After a reload or restart, the deleted store ID is present in Please serialize all store-limit config writers, or use a CAS/retry/merge mechanism that preserves the deletion. Add a deterministic durable-state regression test, for example: type blockingConfigStorage struct {
storage.Storage
entered chan struct{}
release chan struct{}
once sync.Once
}
func (s *blockingConfigStorage) SaveConfig(cfg any) error {
s.once.Do(func() {
close(s.entered)
<-s.release
})
return s.Storage.SaveConfig(cfg)
}
func TestDeleteStoreLimitDoesNotLoseConcurrentAllStoreUpdate(t *testing.T) {
re := require.New(t)
_, opt, err := newTestScheduleConfig()
re.NoError(err)
backend := storage.NewStorageWithMemoryBackend()
rc := newTestRaftCluster(context.Background(), mockid.NewIDAllocator(), opt, backend)
const storeID = uint64(1)
tombstone := core.NewStoreInfo(&metapb.Store{
Id: storeID,
NodeState: metapb.NodeState_Tombstone,
})
rc.PutStore(tombstone)
// Seed a durable per-store limit before installing the barrier.
opt.SetStoreLimit(storeID, storelimit.AddPeer, 60)
re.NoError(backend.SaveStoreMeta(tombstone.GetMeta()))
re.NoError(opt.Persist(backend))
blocked := &blockingConfigStorage{
Storage: backend,
entered: make(chan struct{}),
release: make(chan struct{}),
}
rc.storage = blocked
done := make(chan error, 1)
go func() {
done <- rc.SetAllStoresLimit(storelimit.AddPeer, 120)
}()
<-blocked.entered
// The removal write succeeds while the batch setter is paused.
re.NoError(rc.deleteStore(tombstone))
close(blocked.release)
re.NoError(<-done)
_, reloaded, err := newTestScheduleConfig()
re.NoError(err)
re.NoError(reloaded.Reload(backend))
_, ok := reloaded.GetScheduleConfig().StoreLimit[storeID]
re.False(ok)
}This test fails on the current head because the final write restores
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The barrier in the previous example must allow the second type blockingConfigStorage struct {
storage.Storage
entered chan struct{}
release chan struct{}
first atomic.Bool
}
func (s *blockingConfigStorage) SaveConfig(cfg any) error {
if s.first.CompareAndSwap(false, true) {
close(s.entered)
<-s.release
}
return s.Storage.SaveConfig(cfg)
}
func TestDeleteStoreLimitDoesNotLoseConcurrentAllStoreUpdate(t *testing.T) {
re := require.New(t)
_, opt, err := newTestScheduleConfig()
re.NoError(err)
backend := storage.NewStorageWithMemoryBackend()
rc := newTestRaftCluster(context.Background(), mockid.NewIDAllocator(), opt, backend)
const storeID = uint64(1)
tombstone := core.NewStoreInfo(&metapb.Store{
Id: storeID,
NodeState: metapb.NodeState_Tombstone,
})
rc.PutStore(tombstone)
opt.SetStoreLimit(storeID, storelimit.AddPeer, 60)
re.NoError(backend.SaveStoreMeta(tombstone.GetMeta()))
re.NoError(opt.Persist(backend))
blocked := &blockingConfigStorage{
Storage: backend,
entered: make(chan struct{}),
release: make(chan struct{}),
}
rc.storage = blocked
done := make(chan error, 1)
go func() {
done <- rc.SetAllStoresLimit(storelimit.AddPeer, 120)
}()
<-blocked.entered
// RemoveStoreLimit must successfully write config B while config A is paused.
re.NoError(rc.deleteStore(tombstone))
close(blocked.release)
re.NoError(<-done)
_, reloaded, err := newTestScheduleConfig()
re.NoError(err)
re.NoError(reloaded.Reload(backend))
_, ok := reloaded.GetScheduleConfig().StoreLimit[storeID]
re.False(ok)
}Add
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Confirmed, not fixing here: closing this needs a lock or CAS shared across every store-limit-persisting path (
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This remains blocking on the updated head. The fact that full-config writers were already unsynchronized does not make the new |
||
| storeIDStr := strconv.FormatUint(store.GetID(), 10) | ||
| statistics.DeleteClusterStatusMetrics(store) | ||
| statistics.ResetStoreStatistics(storeIDStr) | ||
|
|
@@ -2621,6 +2628,18 @@ func (c *RaftCluster) loadExternalTS() { | |
|
|
||
| // SetStoreLimit sets a store limit for a given type and rate. | ||
| func (c *RaftCluster) SetStoreLimit(storeID uint64, typ storelimit.Type, ratePerMin float64) error { | ||
| // A tombstoned store's config entry is only ever cleared once, at bury time | ||
| // (RemoveStoreLimit); nothing sweeps it again afterward. Without this check, | ||
| // setting a limit for an already-tombstoned store re-adds it, and | ||
| // StoreLimitGauge stays republished for it until final removal. | ||
| // | ||
| // GetStore returning nil is deliberately NOT treated the same as removed | ||
| // here: callers legitimately set a store's limit before the store itself | ||
| // is registered (see testCluster.addRegionStore), so nil just means | ||
| // "not created yet," not "already gone." | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Confirmed this is reachable via the HTTP path. Not adding a |
||
| if store := c.GetStore(storeID); store != nil && store.IsRemoved() { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This check is not synchronized with
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add a deterministic unit test for the check-then-act race. A concurrent test without a barrier is not sufficient: func TestSetStoreLimitCannotRecreateRemovedStore(t *testing.T) {
re := require.New(t)
// Pause SetStoreLimit immediately after its IsRemoved check.
// The test-only barrier must signal that the check has passed.
done := make(chan error, 1)
go func() {
done <- cluster.SetStoreLimit(storeID, storelimit.AddPeer, 60)
}()
<-afterRemovedCheck
// Bury the store while SetStoreLimit is paused, then release it.
re.NoError(cluster.BuryStore(storeID, true))
close(releaseSetStoreLimit)
re.ErrorIs(<-done, errs.ErrStoreRemoved)
_, ok := cluster.GetOpts().GetScheduleConfig().StoreLimit[storeID]
re.False(ok)
}Please implement the barrier with a test-only failpoint or an equivalent injectable seam, and disable it with cleanup. The test must prove that the operation cannot mutate or persist the tombstoned store after burial; the current check is not atomic with the subsequent config mutation.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Confirmed, tracked in the PR description's Known limitations (updated to also cover the final-removal case raised separately below). Not adding the requested test or synchronization: reusing storeStateLock here would deadlock, since RemoveStore/UpStore already hold it while calling SetStoreLimit internally, and a dedicated lock or locked/unlocked split is more surgery than this narrow window warrants.
bufferflies marked this conversation as resolved.
bufferflies marked this conversation as resolved.
|
||
| return errs.ErrStoreRemoved.FastGenByArgs(storeID) | ||
| } | ||
| old := c.opt.GetScheduleConfig().Clone() | ||
| c.opt.SetStoreLimit(storeID, typ, ratePerMin) | ||
| if err := c.opt.Persist(c.storage); err != nil { | ||
|
|
@@ -2800,6 +2819,15 @@ func (c *RaftCluster) UnsetServiceIndependent(name string) { | |
| const networkSlowStoreEvictThreshold = 99 | ||
|
|
||
| func (c *RaftCluster) adjustNetworkSlowStore(storeID uint64) { | ||
| // The gRPC handler's own IsRemoved() check isn't atomic with this call, so an | ||
| // in-flight StoreHeartbeat that already passed it can still reach here after | ||
| // bury. BuryStoreLocked only clears storeTriggerNetworkSlowEvict once, so a | ||
| // re-check here (unlike the metric write below, which has no such guard) is | ||
| // the only thing that can stop it from staying republished until final | ||
| // removal. | ||
| if store := c.GetStore(storeID); store == nil || store.IsRemoved() { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This recheck is not atomic with
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add a deterministic unit test for the equivalent race in adjustNetworkSlowStore: func TestAdjustNetworkSlowStoreCannotRepublishAfterBury(t *testing.T) {
re := require.New(t)
defer storeTriggerNetworkSlowEvict.Reset()
// Seed a score at or above networkSlowStoreEvictThreshold.
// Pause after the removed-store recheck and before TriggerNetworkSlowEvict.
done := make(chan struct{})
go func() {
cluster.adjustNetworkSlowStore(storeID)
close(done)
}()
<-afterNetworkSlowCheck
re.NoError(cluster.BuryStore(storeID, true))
close(releaseNetworkSlowStore)
<-done
re.Zero(testutil.ToFloat64(
storeTriggerNetworkSlowEvict.WithLabelValues(strconv.FormatUint(storeID, 10)),
))
}The barrier must be test-only and deterministic. The current recheck can pass, burial can delete the label, and the in-flight call can then increment the counter again. The assertion must inspect the metric after the in-flight call completes.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Confirmed, same non-atomicity as SetStoreLimit above, but bounded: deleteStore's storeTriggerNetworkSlowEvict.DeleteLabelValues call runs on both the manual remove-tombstone and 30-day auto-GC paths, so any recreated series is cleared by final removal at the latest. Not adding synchronization for the same reason as the SetStoreLimit case. |
||
| return | ||
| } | ||
| if c.GetAvgNetworkSlowScore(storeID) >= networkSlowStoreEvictThreshold { | ||
| c.TriggerNetworkSlowEvict(storeID) | ||
| storeTriggerNetworkSlowEvict.WithLabelValues(strconv.FormatUint(storeID, 10)).Inc() | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.