1818import com .google .common .collect .Maps ;
1919import com .google .gson .annotations .SerializedName ;
2020import com .starrocks .catalog .Database ;
21- import com .starrocks .catalog .MaterializedIndex ;
22- import com .starrocks .catalog .MaterializedIndex .IndexExtState ;
2321import com .starrocks .catalog .OlapTable ;
24- import com .starrocks .catalog .Partition ;
25- import com .starrocks .catalog .PhysicalPartition ;
2622import com .starrocks .catalog .Table ;
2723import com .starrocks .catalog .Tablet ;
2824import com .starrocks .common .Config ;
2925import com .starrocks .common .StarRocksException ;
3026import com .starrocks .common .util .FrontendDaemon ;
31- import com .starrocks .common .util .concurrent .lock .LockType ;
32- import com .starrocks .common .util .concurrent .lock .Locker ;
3327import com .starrocks .lake .LakeTablet ;
3428import com .starrocks .metric .MetricRepo ;
3529import com .starrocks .persist .ImageWriter ;
5246import java .util .ArrayList ;
5347import java .util .List ;
5448import java .util .Map ;
55- import java .util .Set ;
5649import java .util .concurrent .ConcurrentHashMap ;
5750
5851public class TabletReshardJobMgr extends FrontendDaemon implements GsonPostProcessable {
@@ -75,13 +68,23 @@ public class TabletReshardJobMgr extends FrontendDaemon implements GsonPostProce
7568 public record ReshardCandidateKey (long dbId , long tableId ) {
7669 }
7770
78- private final Set <ReshardCandidateKey > reshardCandidates = ConcurrentHashMap . newKeySet ();
71+ private final Map <ReshardCandidateKey , ReshardSignals > reshardCandidates = new ConcurrentHashMap <> ();
7972
80- public void markReshardCandidate (long dbId , long tableId ) {
73+ // Enqueue a table whose just-published partition already crossed a split/merge threshold,
74+ // carrying the precomputed signal so the drain triggers without re-walking the whole table.
75+ // Concurrent marks for the same table before the next drain coalesce by max/min.
76+ public void markReshardCandidate (long dbId , long tableId ,
77+ long maxTabletSize , long minAdjacentTabletPairSize ) {
8178 if (!isLeaderAdmissionOpen ()) {
8279 return ;
8380 }
84- reshardCandidates .add (new ReshardCandidateKey (dbId , tableId ));
81+ ReshardSignals fresh = new ReshardSignals ();
82+ fresh .maxTabletSize = maxTabletSize ;
83+ fresh .minAdjacentTabletPairSize = minAdjacentTabletPairSize ;
84+ reshardCandidates .merge (new ReshardCandidateKey (dbId , tableId ), fresh , (old , incoming ) -> {
85+ old .merge (incoming );
86+ return old ;
87+ });
8588 }
8689
8790 public TabletReshardJobMgr () {
@@ -125,88 +128,73 @@ public void createTabletReshardJob(Database db, OlapTable table, MergeTabletClau
125128 addTabletReshardJob (job );
126129 }
127130
128- private static class ReshardSignals {
129- long maxTabletSize = 0L ;
130- long minAdjacentTabletPairSize = Long .MAX_VALUE ;
131+ public static class ReshardSignals {
132+ public long maxTabletSize = 0L ;
133+ public long minAdjacentTabletPairSize = Long .MAX_VALUE ;
134+
135+ void merge (ReshardSignals other ) {
136+ maxTabletSize = Math .max (maxTabletSize , other .maxTabletSize );
137+ minAdjacentTabletPairSize = Math .min (minAdjacentTabletPairSize , other .minAdjacentTabletPairSize );
138+ }
139+ }
140+
141+ // Accumulate split/merge signals over one VISIBLE index's tablets into {@code signals}.
142+ // maxTabletSize drives split (never gated); the adjacent-pair min drives merge and only
143+ // contributes when the index is above the parallelism floor and both neighbors are fresh
144+ // (LakeTablet dataSizeUpdateTime >= visibleVersionTime), so auto-merge cannot shrink an
145+ // index below its pre-split parallelism. Pure in-memory field reads, no RPC. Called by the
146+ // periodic TabletStatMgr scan (whole table) and the publish apply (one partition), both of
147+ // which already iterate these tablets, so the signal is precomputed instead of re-walked
148+ // under a fresh lock.
149+ public static void accumulateIndexSignals (List <Tablet > tablets , int parallelismFloor ,
150+ long visibleVersionTime , ReshardSignals signals ) {
151+ boolean eligibleForMerge = tablets .size () > parallelismFloor ;
152+ long prevFreshTabletSize = -1L ;
153+ for (Tablet tablet : tablets ) {
154+ long dataSize = tablet .getDataSize (true );
155+ signals .maxTabletSize = Math .max (signals .maxTabletSize , dataSize );
156+ if (!(tablet instanceof LakeTablet )
157+ || ((LakeTablet ) tablet ).getDataSizeUpdateTime () < visibleVersionTime ) {
158+ prevFreshTabletSize = -1L ;
159+ continue ;
160+ }
161+ if (prevFreshTabletSize >= 0 && eligibleForMerge ) {
162+ signals .minAdjacentTabletPairSize = Math .min (signals .minAdjacentTabletPairSize ,
163+ prevFreshTabletSize + dataSize );
164+ }
165+ prevFreshTabletSize = dataSize ;
166+ }
167+ }
168+
169+ // Parallelism floor for merge eligibility; returns 0 (ungated) when warehouse state is unavailable.
170+ public static int safeComputeParallelismFloor (long tableId ) {
171+ try {
172+ return TabletReshardUtils .computeParallelismFloor (tableId );
173+ } catch (RuntimeException e ) {
174+ LOG .warn ("Parallelism floor unavailable for table {}; auto-merge will not be floor-gated." , tableId , e );
175+ return 0 ;
176+ }
131177 }
132178
133179 /**
134180 * Shared reshard-trigger entrypoint used by both the publish-driven candidate drain and the
135- * periodic TabletStatMgr scan. Self-gates on leader/admission, cloud-native range distribution,
136- * and NORMAL table state; recomputes split/merge signals from in-memory LakeTablet stats (no RPC).
181+ * periodic TabletStatMgr scan, with split/merge signals precomputed by the caller (which is
182+ * already walking the relevant tablets — no second lock or full-table re-walk here). Self-gates
183+ * on leader/admission, cloud-native range distribution, and NORMAL table state; the authoritative
184+ * NORMAL re-check happens in the job factory under its own lock.
137185 */
138- public void triggerTabletReshardForTable (Database db , OlapTable table ) {
186+ public void triggerTabletReshardForTable (Database db , OlapTable table ,
187+ long maxTabletSize , long minAdjacentTabletPairSize ) {
139188 if (!isLeaderAdmissionOpen ()) {
140189 return ;
141190 }
142191 if (!table .isCloudNativeTableOrMaterializedView () || !table .isRangeDistribution ()) {
143192 return ;
144193 }
145- // Cheap pre-check; the authoritative NORMAL check happens under the read lock in computeReshardSignals.
146194 if (table .getState () != OlapTable .OlapTableState .NORMAL ) {
147195 return ;
148196 }
149- ReshardSignals signals = computeReshardSignals (db , table );
150- if (signals == null ) {
151- return ;
152- }
153- triggerTabletReshard (db , table , signals .maxTabletSize , signals .minAdjacentTabletPairSize );
154- }
155-
156- // Returns null if the table is no longer reshard-eligible (state changed under the lock).
157- private ReshardSignals computeReshardSignals (Database db , OlapTable table ) {
158- int parallelismFloor = resolveMergeParallelismFloor (db , table ); // resolves warehouse state before the lock
159- ReshardSignals signals = new ReshardSignals ();
160- Locker locker = new Locker ();
161- locker .lockTableWithIntensiveDbLock (db .getId (), table .getId (), LockType .READ );
162- try {
163- if (table .getState () != OlapTable .OlapTableState .NORMAL ) {
164- return null ;
165- }
166- for (Partition partition : table .getAllPartitions ()) {
167- for (PhysicalPartition physicalPartition : partition .getSubPartitions ()) {
168- long visibleVersionTime = physicalPartition .getVisibleVersionTime ();
169- for (MaterializedIndex index :
170- physicalPartition .getLatestMaterializedIndices (IndexExtState .VISIBLE )) {
171- List <Tablet > tablets = index .getTablets ();
172- // Only an index above the parallelism floor contributes the merge signal;
173- // otherwise auto-merge could shrink it below the pre-split parallelism count.
174- // Split detection (maxTabletSize) is never gated.
175- boolean eligibleForMerge = tablets .size () > parallelismFloor ;
176- long prevFreshTabletSize = -1L ;
177- for (Tablet tablet : tablets ) {
178- long dataSize = tablet .getDataSize (true );
179- signals .maxTabletSize = Math .max (signals .maxTabletSize , dataSize );
180- if (!(tablet instanceof LakeTablet )
181- || ((LakeTablet ) tablet ).getDataSizeUpdateTime () < visibleVersionTime ) {
182- prevFreshTabletSize = -1L ;
183- continue ;
184- }
185- if (prevFreshTabletSize >= 0 && eligibleForMerge ) {
186- signals .minAdjacentTabletPairSize = Math .min (signals .minAdjacentTabletPairSize ,
187- prevFreshTabletSize + dataSize );
188- }
189- prevFreshTabletSize = dataSize ;
190- }
191- }
192- }
193- }
194- } finally {
195- locker .unLockTableWithIntensiveDbLock (db .getId (), table .getId (), LockType .READ );
196- }
197- return signals ;
198- }
199-
200- // Precondition: caller has verified the table is a cloud-native range-distribution table.
201- private int resolveMergeParallelismFloor (Database db , OlapTable table ) {
202- try {
203- return TabletReshardUtils .computeParallelismFloor (table .getId ());
204- } catch (RuntimeException e ) {
205- LOG .warn ("Parallelism floor unavailable for table {}.{}; "
206- + "auto-merge will not be floor-gated for this table." ,
207- db .getFullName (), table .getName (), e );
208- return 0 ;
209- }
197+ triggerTabletReshard (db , table , maxTabletSize , minAdjacentTabletPairSize );
210198 }
211199
212200 // Precondition: caller has verified the table is a cloud-native range-distribution table.
@@ -340,9 +328,12 @@ private void drainReshardCandidates() {
340328 if (reshardCandidates .isEmpty ()) {
341329 return ;
342330 }
343- List <ReshardCandidateKey > batch = new ArrayList <>(reshardCandidates );
344- reshardCandidates .removeAll (batch );
345- for (ReshardCandidateKey key : batch ) {
331+ // Snapshot the keys; remove each atomically so a concurrent re-mark is re-evaluated next tick.
332+ for (ReshardCandidateKey key : new ArrayList <>(reshardCandidates .keySet ())) {
333+ ReshardSignals signals = reshardCandidates .remove (key );
334+ if (signals == null ) {
335+ continue ;
336+ }
346337 // db and table lookups are not atomic; the null guards are conservative — a dropped db/table
347338 // is simply skipped this cycle.
348339 Database db = GlobalStateMgr .getCurrentState ().getLocalMetastore ().getDb (key .dbId ());
@@ -354,7 +345,8 @@ private void drainReshardCandidates() {
354345 if (!(table instanceof OlapTable )) {
355346 continue ;
356347 }
357- triggerTabletReshardForTable (db , (OlapTable ) table );
348+ triggerTabletReshardForTable (db , (OlapTable ) table ,
349+ signals .maxTabletSize , signals .minAdjacentTabletPairSize );
358350 }
359351 }
360352
0 commit comments