4444import com .starrocks .catalog .Tablet ;
4545import com .starrocks .common .Config ;
4646import com .starrocks .common .ErrorCode ;
47+ import com .starrocks .common .NoAliveBackendException ;
4748import com .starrocks .common .StarRocksException ;
4849import com .starrocks .common .ThreadPoolManager ;
4950import com .starrocks .common .util .FrontendDaemon ;
5556import com .starrocks .lake .Utils ;
5657import com .starrocks .lake .compaction .Quantiles ;
5758import com .starrocks .metric .MetricRepo ;
59+ import com .starrocks .proto .AggregatePublishVersionRequest ;
5860import com .starrocks .proto .DeleteTxnLogRequest ;
5961import com .starrocks .proto .DeleteTxnLogResponse ;
6062import com .starrocks .proto .TabletStatPB ;
6163import com .starrocks .proto .TxnInfoPB ;
64+ import com .starrocks .proto .TxnTypePB ;
6265import com .starrocks .rpc .BrpcProxy ;
6366import com .starrocks .rpc .LakeService ;
67+ import com .starrocks .rpc .RpcException ;
6468import com .starrocks .server .GlobalStateMgr ;
6569import com .starrocks .server .RunMode ;
6670import com .starrocks .server .WarehouseManager ;
@@ -597,6 +601,7 @@ public boolean publishPartitionBatch(Database db, long tableId, PartitionPublish
597601 // The mapping is shadow index id -> ShadowIndexTxnBatch
598602 Map <Long , ShadowIndexTxnBatch > shadowIndexTxnBatches = null ;
599603 Set <Tablet > normalTablets = null ;
604+ List <Tablet > carryForwardTablets = null ;
600605
601606 Locker locker = new Locker ();
602607 locker .lockTablesWithIntensiveDbLock (db .getId (), Lists .newArrayList (tableId ), LockType .READ );
@@ -626,6 +631,7 @@ public boolean publishPartitionBatch(Database db, long tableId, PartitionPublish
626631 }
627632
628633 useAggregatePublish = table .isFileBundling ();
634+ Set <Long > publishedNormalIndexIds = Sets .newHashSet ();
629635 for (int i = 0 ; i < transactionStates .size (); i ++) {
630636 TransactionState txnState = transactionStates .get (i );
631637 computeResource = txnState .getComputeResource ();
@@ -650,9 +656,21 @@ public boolean publishPartitionBatch(Database db, long tableId, PartitionPublish
650656 } else {
651657 normalTablets = (normalTablets == null ) ? Sets .newHashSet () : normalTablets ;
652658 normalTablets .addAll (index .getTablets ());
659+ publishedNormalIndexIds .add (index .getId ());
653660 }
654661 }
655662 }
663+ // File bundling stores the metadata of ALL tablets of a partition version in a single bundle file.
664+ // The batched transactions' index views are point-in-time (for lake compaction snapshotted at txn
665+ // begin); if a rollup/MV index became visible after they began, none of them touch it, so publishing
666+ // the new version would write a bundle WITHOUT that index's tablets and permanently wedge the
667+ // partition. Carry forward every currently-visible NORMAL index none of these transactions touched
668+ // so the new bundle stays a complete whole-partition snapshot. See collectFileBundlingCarryForwardTablets.
669+ if (useAggregatePublish ) {
670+ carryForwardTablets = collectFileBundlingCarryForwardTablets (
671+ partition .getLatestMaterializedIndices (MaterializedIndex .IndexExtState .VISIBLE ),
672+ publishedNormalIndexIds );
673+ }
656674 } finally {
657675 locker .unLockTablesWithIntensiveDbLock (db .getId (), Lists .newArrayList (tableId ), LockType .READ );
658676 }
@@ -696,6 +714,10 @@ public boolean publishPartitionBatch(Database db, long tableId, PartitionPublish
696714 Utils .publishVersionBatch (publishTablets , txnInfos ,
697715 startVersion - 1 , endVersion , compactionScores , nodeToTablets ,
698716 computeResource , tabletStats );
717+ } else if (CollectionUtils .isNotEmpty (carryForwardTablets )) {
718+ aggregatePublishWithCarryForward (publishTablets , txnInfos , carryForwardTablets ,
719+ startVersion - 1 , endVersion , nodeToTablets , computeResource , compactionScores ,
720+ tabletStats );
699721 } else {
700722 Utils .aggregatePublishVersion (publishTablets , txnInfos , startVersion - 1 , endVersion ,
701723 compactionScores , nodeToTablets , computeResource , tabletStats );
@@ -1038,6 +1060,7 @@ private boolean publishPartition(@NotNull Database db, @NotNull TableCommitInfo
10381060 ComputeResource computeResource = txnState .getComputeResource ();
10391061 List <Tablet > normalTablets = null ;
10401062 List <Tablet > shadowTablets = null ;
1063+ List <Tablet > carryForwardTablets = null ;
10411064
10421065 Locker locker = new Locker ();
10431066 locker .lockTablesWithIntensiveDbLock (db .getId (), Lists .newArrayList (tableId ), LockType .READ );
@@ -1064,6 +1087,7 @@ private boolean publishPartition(@NotNull Database db, @NotNull TableCommitInfo
10641087 }
10651088 baseVersion = partition .getVisibleVersion ();
10661089 List <MaterializedIndex > indexes = txnState .getPartitionLoadedIndexes (table .getId (), partition );
1090+ Set <Long > publishedNormalIndexIds = Sets .newHashSet ();
10671091 for (MaterializedIndex index : indexes ) {
10681092 if (!index .visibleForTransaction (txnId )) {
10691093 LOG .info ("Ignored index {} for transaction {}" , table .getIndexNameByMetaId (index .getMetaId ()), txnId );
@@ -1075,8 +1099,23 @@ private boolean publishPartition(@NotNull Database db, @NotNull TableCommitInfo
10751099 } else {
10761100 normalTablets = (normalTablets == null ) ? Lists .newArrayList () : normalTablets ;
10771101 normalTablets .addAll (index .getTablets ());
1102+ publishedNormalIndexIds .add (index .getId ());
10781103 }
10791104 }
1105+ // File bundling stores the metadata of ALL tablets of a partition version in a single bundle
1106+ // file (meta/0_<version>.meta). The index set above comes from the transaction's point-in-time
1107+ // view (for lake compaction it is snapshotted at txn begin). If a materialized-view/rollup
1108+ // index became visible after this transaction began, that stale view omits it, so publishing
1109+ // the new version would write a bundle WITHOUT the rollup index's tablets, dropping them from
1110+ // this version and permanently wedging the partition's publish queue (a later publish reading
1111+ // those tablets at this version gets a 404). Carry forward every currently-visible NORMAL index
1112+ // this transaction did not touch, so the new bundle remains a complete whole-partition snapshot.
1113+ // Only needed for file bundling; without it each tablet keeps its own per-version metadata file.
1114+ if (useAggregatePublish ) {
1115+ carryForwardTablets = collectFileBundlingCarryForwardTablets (
1116+ partition .getLatestMaterializedIndices (MaterializedIndex .IndexExtState .VISIBLE ),
1117+ publishedNormalIndexIds );
1118+ }
10801119 } finally {
10811120 locker .unLockTablesWithIntensiveDbLock (db .getId (), Lists .newArrayList (tableId ), LockType .READ );
10821121 }
@@ -1092,8 +1131,13 @@ private boolean publishPartition(@NotNull Database db, @NotNull TableCommitInfo
10921131 // Per-tablet stats from the publish response: first-load row counts for statistics
10931132 // collection, and range-distribution tablet sizes for real-time reshard triggering.
10941133 Map <Long , TabletStatPB > tabletStats = new HashMap <>();
1095- Utils .publishVersion (normalTablets , txnInfo , baseVersion , txnVersion , compactionScores ,
1096- computeResource , tabletStats , useAggregatePublish );
1134+ if (useAggregatePublish && CollectionUtils .isNotEmpty (carryForwardTablets )) {
1135+ aggregatePublishWithCarryForward (normalTablets , Lists .newArrayList (txnInfo ), carryForwardTablets ,
1136+ baseVersion , txnVersion , null , computeResource , compactionScores , tabletStats );
1137+ } else {
1138+ Utils .publishVersion (normalTablets , txnInfo , baseVersion , txnVersion , compactionScores ,
1139+ computeResource , tabletStats , useAggregatePublish );
1140+ }
10971141
10981142 Quantiles quantiles = Quantiles .compute (compactionScores .values ());
10991143 partitionCommitInfo .setCompactionScore (quantiles );
@@ -1120,6 +1164,67 @@ private boolean publishPartition(@NotNull Database db, @NotNull TableCommitInfo
11201164 }
11211165 }
11221166
1167+ // For file bundling only: from the partition's currently-visible indexes, return the tablets of every
1168+ // NORMAL index that this transaction did not touch (i.e. whose index id is not in publishedNormalIndexIds).
1169+ // These must be carried forward (empty version bump) into the new version's bundle so it stays a complete
1170+ // whole-partition snapshot; otherwise a transaction with a stale index view (notably lake compaction
1171+ // whose loaded-index set is snapshotted at txn begin, before a rollup/MV index became visible) would
1172+ // publish a bundle missing that index and permanently wedge the partition. Returns null when nothing
1173+ // needs carrying forward (e.g. a normal load already covers every visible index). Caller must hold the
1174+ // table read lock while obtaining visibleIndexes.
1175+ static List <Tablet > collectFileBundlingCarryForwardTablets (List <MaterializedIndex > visibleIndexes ,
1176+ Set <Long > publishedNormalIndexIds ) {
1177+ List <Tablet > carryForwardTablets = null ;
1178+ for (MaterializedIndex index : visibleIndexes ) {
1179+ if (index .getState () == MaterializedIndex .IndexState .SHADOW
1180+ || publishedNormalIndexIds .contains (index .getId ())) {
1181+ continue ;
1182+ }
1183+ carryForwardTablets = (carryForwardTablets == null ) ? Lists .newArrayList () : carryForwardTablets ;
1184+ carryForwardTablets .addAll (index .getTablets ());
1185+ }
1186+ return carryForwardTablets ;
1187+ }
1188+
1189+ // File bundling only. Publish the touched tablets (this publish's real transactions) together with the
1190+ // carry-forward tablets (untouched but currently-visible indexes) in ONE aggregate request, so the whole
1191+ // partition's metadata for the new version lands in a single bundle file (meta/0_<newVersion>.meta). Two
1192+ // separate aggregate publishes would each truncate-write that same bundle and lose one of the two sets.
1193+ //
1194+ // The carry-forward tablets have no txn log for these transactions, so for every real transaction we emit a
1195+ // matching no-op empty transaction (no_op_publish=true): the BE bypasses log loading and advances the tablet
1196+ // version without any data change. The count must match the real transactions because the BE requires
1197+ // new_version == base_version + txns.size() for a multi-transaction batch (a batch of pre-rollup compactions
1198+ // can span several versions).
1199+ static void aggregatePublishWithCarryForward (List <Tablet > touchedTablets , List <TxnInfoPB > txnInfos ,
1200+ List <Tablet > carryForwardTablets , long baseVersion ,
1201+ long newVersion , Map <ComputeNode , List <Long >> nodeToTablets ,
1202+ ComputeResource computeResource ,
1203+ Map <Long , Double > compactionScores ,
1204+ Map <Long , TabletStatPB > tabletStats )
1205+ throws NoAliveBackendException , RpcException {
1206+ AggregatePublishVersionRequest request = new AggregatePublishVersionRequest ();
1207+ Utils .createSubRequestForAggregatePublish (touchedTablets , txnInfos , baseVersion , newVersion ,
1208+ nodeToTablets , computeResource , request );
1209+
1210+ List <TxnInfoPB > carryForwardTxnInfos = Lists .newArrayListWithCapacity (txnInfos .size ());
1211+ for (TxnInfoPB txnInfo : txnInfos ) {
1212+ TxnInfoPB emptyTxnInfo = new TxnInfoPB ();
1213+ emptyTxnInfo .txnId = -1L ;
1214+ emptyTxnInfo .combinedTxnLog = false ;
1215+ emptyTxnInfo .commitTime = txnInfo .commitTime ;
1216+ emptyTxnInfo .txnType = TxnTypePB .TXN_EMPTY ;
1217+ emptyTxnInfo .gtid = txnInfo .gtid ;
1218+ emptyTxnInfo .noOpPublish = true ;
1219+ carryForwardTxnInfos .add (emptyTxnInfo );
1220+ }
1221+ // The carry-forward tablets have no txn log to delete on success, so do not thread nodeToTablets here.
1222+ Utils .createSubRequestForAggregatePublish (carryForwardTablets , carryForwardTxnInfos , baseVersion , newVersion ,
1223+ null , computeResource , request );
1224+ Utils .sendAggregatePublishVersionRequest (request , baseVersion , computeResource , compactionScores ,
1225+ tabletStats );
1226+ }
1227+
11231228 // Per-partition publishPartition phase breakdown for slow outliers.
11241229 // total = executor_queue + db_lock_wait + fe_prep + rpc, where:
11251230 // executor_queue = CompletableFuture submit -> lambda entry
0 commit comments