@@ -34,6 +34,9 @@ import (
3434
3535const (
3636 splitScatterPendingLimit = 1024
37+ // The actual retry cadence is also bounded by the checker dispatch loop. This
38+ // is only the minimum interval to avoid retrying the same pending item too
39+ // frequently when checker ticks are fast.
3740 splitScatterRetryBackoff = time .Second
3841 splitScatterPendingTTL = 3 * time .Minute
3942)
@@ -53,7 +56,10 @@ type splitScatterController struct {
5356 regionScatterer * scatter.RegionScatterer
5457
5558 pendingMu syncutil.RWMutex
56- pending map [uint64 ]splitScatterPendingItem
59+ // pending maps a pending region ID to its latest split-scatter batch item.
60+ // The item keeps its batch group so stale snapshots cannot mutate a newer
61+ // pending entry for the same region.
62+ pending map [uint64 ]splitScatterPendingItem
5763}
5864
5965func newSplitScatterController (
@@ -90,10 +96,10 @@ func (c *splitScatterController) collectTopPendingSplitScatter(limit int) []spli
9096 // to block pending updates. Stale snapshots are safe because delay/delete
9197 // recheck regionID + group before mutating pending.
9298 pendingSnapshot := make ([]splitScatterPendingItem , 0 , len (c .pending ))
93- expiredRegionIDs := make ([]uint64 , 0 )
94- for regionID , pending := range c .pending {
99+ expiredSnapshot := make ([]splitScatterPendingItem , 0 )
100+ for _ , pending := range c .pending {
95101 if ! pending .expireAt .IsZero () && ! now .Before (pending .expireAt ) {
96- expiredRegionIDs = append (expiredRegionIDs , regionID )
102+ expiredSnapshot = append (expiredSnapshot , pending )
97103 continue
98104 }
99105 pendingSnapshot = append (pendingSnapshot , pending )
@@ -124,6 +130,9 @@ func (c *splitScatterController) collectTopPendingSplitScatter(limit int) []spli
124130 candidates = append (candidates , pending )
125131 }
126132 sort .Slice (candidates , func (i , j int ) bool {
133+ if ! candidates [i ].expireAt .Equal (candidates [j ].expireAt ) {
134+ return candidates [i ].expireAt .Before (candidates [j ].expireAt )
135+ }
127136 if candidates [i ].group != candidates [j ].group {
128137 return candidates [i ].group < candidates [j ].group
129138 }
@@ -133,16 +142,13 @@ func (c *splitScatterController) collectTopPendingSplitScatter(limit int) []spli
133142 candidates = candidates [:limit ]
134143 }
135144
136- if len (expiredRegionIDs ) > 0 {
145+ if len (expiredSnapshot ) > 0 {
137146 expiredCount := 0
138147 c .pendingMu .Lock ()
139- for _ , regionID := range expiredRegionIDs {
140- pending , ok := c .pending [regionID ]
141- if ! ok {
142- continue
143- }
144- if ! pending .expireAt .IsZero () && ! now .Before (pending .expireAt ) {
145- delete (c .pending , regionID )
148+ for _ , expired := range expiredSnapshot {
149+ pending , ok := c .pending [expired .regionID ]
150+ if ok && pending .group == expired .group && ! pending .expireAt .IsZero () && ! now .Before (pending .expireAt ) {
151+ delete (c .pending , expired .regionID )
146152 expiredCount ++
147153 }
148154 }
@@ -175,7 +181,8 @@ func (c *splitScatterController) deletePendingSplitScatter(expected splitScatter
175181 delete (c .pending , expected .regionID )
176182}
177183
178- func (c * splitScatterController ) removeExpiredPendingSplitScatterLocked (now time.Time ) int {
184+ func (c * splitScatterController ) removeExpiredPendingSplitScatterLocked () int {
185+ now := time .Now ()
179186 expiredCount := 0
180187 for regionID , pending := range c .pending {
181188 if ! pending .expireAt .IsZero () && ! now .Before (pending .expireAt ) {
@@ -207,7 +214,7 @@ func (c *splitScatterController) recordSplitScatterBatch(sourceRegionID uint64,
207214 }
208215 c .pendingMu .Lock ()
209216 defer c .pendingMu .Unlock ()
210- if expiredCount := c .removeExpiredPendingSplitScatterLocked (time . Now () ); expiredCount > 0 {
217+ if expiredCount := c .removeExpiredPendingSplitScatterLocked (); expiredCount > 0 {
211218 splitScatterPendingExpiredCounter .Add (float64 (expiredCount ))
212219 }
213220 newPendingCount := 0
@@ -220,6 +227,9 @@ func (c *splitScatterController) recordSplitScatterBatch(sourceRegionID uint64,
220227 }
221228 }
222229 if len (c .pending )+ newPendingCount > splitScatterPendingLimit {
230+ // Keep each split batch atomic. Recording only part of a source/child
231+ // batch would make the scatter group incomplete, so skip the whole batch
232+ // when the remaining capacity cannot fit it.
223233 splitScatterPendingDroppedCounter .Add (float64 (newPendingCount ))
224234 log .Info ("skip recording split scatter batch due to pending limit" ,
225235 zap .Uint64 ("source-region-id" , sourceRegionID ),
0 commit comments