2121import java .io .IOException ;
2222import java .util .Arrays ;
2323import java .util .Collection ;
24+ import java .util .Comparator ;
2425import java .util .List ;
2526import java .util .Map ;
2627import java .util .NavigableMap ;
@@ -111,6 +112,17 @@ public void commit(Collection<CommitRequest<DynamicCommittable>> commitRequests)
111112 }
112113
113114 /*
115+ Group the incoming commit requests into a three-level structure before committing:
116+
117+ Map<TableKey, Map<JobOperatorKey, NavigableMap<Long, List<CommitRequest>>>>
118+ | | | |
119+ | | | +-- commit requests at that checkpoint
120+ | | +-- checkpointId, sorted ascending so older commits go first
121+ | +-- (jobId, operatorId) of the producing aggregator; deduplication against the
122+ | table's snapshot summaries is per (jobId, operatorId), so each group is
123+ | walked against the ancestor chain independently
124+ +-- (tableName, branch); we load the table and its ancestor chain once per TableKey
125+
114126 Each (table, branch, checkpoint) triplet must have only one commit request.
115127 There may be commit requests from previous checkpoints which have not been committed yet.
116128
@@ -119,43 +131,58 @@ public void commit(Collection<CommitRequest<DynamicCommittable>> commitRequests)
119131 DynamicWriteResultAggregator. Iceberg 1.12 will remove this, and users should upgrade to the 1.11 release first
120132 to migrate their state to a single commit request per checkpoint.
121133 */
122- Map <TableKey , NavigableMap <Long , List <CommitRequest <DynamicCommittable >>>> commitRequestMap =
123- Maps .newHashMap ();
134+ Map <TableKey , Map < JobOperatorKey , NavigableMap <Long , List <CommitRequest <DynamicCommittable >>>>>
135+ commitRequestMap = Maps .newHashMap ();
124136 for (CommitRequest <DynamicCommittable > request : commitRequests ) {
125- NavigableMap < Long , List < CommitRequest < DynamicCommittable >>> committables =
126- commitRequestMap . computeIfAbsent (
127- new TableKey ( request . getCommittable ()) , unused -> Maps .newTreeMap ());
128- committables
129- .computeIfAbsent (request . getCommittable () .checkpointId (), unused -> Lists .newArrayList ())
137+ DynamicCommittable committable = request . getCommittable ();
138+ commitRequestMap
139+ . computeIfAbsent ( committable . key () , unused -> Maps .newHashMap ())
140+ . computeIfAbsent ( new JobOperatorKey ( committable ), unused -> Maps . newTreeMap ())
141+ .computeIfAbsent (committable .checkpointId (), unused -> Lists .newArrayList ())
130142 .add (request );
131143 }
132144
133- for (Map .Entry <TableKey , NavigableMap <Long , List <CommitRequest <DynamicCommittable >>>> entry :
134- commitRequestMap .entrySet ()) {
135- Table table = catalog .loadTable (TableIdentifier .parse (entry .getKey ().tableName ()));
136- DynamicCommittable last = entry .getValue ().lastEntry ().getValue ().get (0 ).getCommittable ();
137- Snapshot latestSnapshot = table .snapshot (entry .getKey ().branch ());
145+ for (Map .Entry <
146+ TableKey ,
147+ Map <JobOperatorKey , NavigableMap <Long , List <CommitRequest <DynamicCommittable >>>>>
148+ tableEntry : commitRequestMap .entrySet ()) {
149+ TableKey tableKey = tableEntry .getKey ();
150+ Table table = catalog .loadTable (TableIdentifier .parse (tableKey .tableName ()));
151+ Snapshot latestSnapshot = table .snapshot (tableKey .branch ());
138152 Iterable <Snapshot > ancestors =
139153 latestSnapshot != null
140154 ? SnapshotUtil .ancestorsOf (latestSnapshot .snapshotId (), table ::snapshot )
141155 : List .of ();
142- long maxCommittedCheckpointId =
143- getMaxCommittedCheckpointId (ancestors , last .jobId (), last .operatorId ());
144-
145- NavigableMap <Long , List <CommitRequest <DynamicCommittable >>> skippedCommitRequests =
146- entry .getValue ().headMap (maxCommittedCheckpointId , true );
147- LOG .debug (
148- "Skipping {} commit requests: {}" , skippedCommitRequests .size (), skippedCommitRequests );
149- // Mark the already committed FilesCommittable(s) as finished
150- skippedCommitRequests
151- .values ()
152- .forEach (list -> list .forEach (CommitRequest ::signalAlreadyCommitted ));
153-
154- NavigableMap <Long , List <CommitRequest <DynamicCommittable >>> uncommitted =
155- entry .getValue ().tailMap (maxCommittedCheckpointId , false );
156- if (!uncommitted .isEmpty ()) {
157- commitPendingRequests (
158- table , entry .getKey ().branch (), uncommitted , last .jobId (), last .operatorId ());
156+
157+ List <Map .Entry <JobOperatorKey , NavigableMap <Long , List <CommitRequest <DynamicCommittable >>>>>
158+ jobEntries = Lists .newArrayList (tableEntry .getValue ().entrySet ());
159+ // Preserve checkpoint order across groups so that older-jobId commits land before newer-jobId
160+ // ones when the batch mixes committables from different jobIds (e.g. state replay after a
161+ // restart). Within a (jobId, operatorId) group, checkpoint order is already guaranteed by
162+ // the inner NavigableMap.
163+ jobEntries .sort (Comparator .comparingLong (entry -> entry .getValue ().firstKey ()));
164+
165+ for (Map .Entry <JobOperatorKey , NavigableMap <Long , List <CommitRequest <DynamicCommittable >>>>
166+ jobEntry : jobEntries ) {
167+ JobOperatorKey jobKey = jobEntry .getKey ();
168+ long maxCommittedCheckpointId =
169+ getMaxCommittedCheckpointId (ancestors , jobKey .jobId (), jobKey .operatorId ());
170+
171+ NavigableMap <Long , List <CommitRequest <DynamicCommittable >>> skippedCommitRequests =
172+ jobEntry .getValue ().headMap (maxCommittedCheckpointId , true );
173+ LOG .debug (
174+ "Skipping {} commit requests: {}" , skippedCommitRequests .size (), skippedCommitRequests );
175+ // Mark the already committed FilesCommittable(s) as finished
176+ skippedCommitRequests
177+ .values ()
178+ .forEach (list -> list .forEach (CommitRequest ::signalAlreadyCommitted ));
179+
180+ NavigableMap <Long , List <CommitRequest <DynamicCommittable >>> uncommitted =
181+ jobEntry .getValue ().tailMap (maxCommittedCheckpointId , false );
182+ if (!uncommitted .isEmpty ()) {
183+ commitPendingRequests (
184+ table , tableKey .branch (), uncommitted , jobKey .jobId (), jobKey .operatorId ());
185+ }
159186 }
160187 }
161188 }
0 commit comments