|
26 | 26 | import java.util.function.Supplier; |
27 | 27 | import org.apache.iceberg.LocationProviders; |
28 | 28 | import org.apache.iceberg.MetadataUpdate; |
| 29 | +import org.apache.iceberg.SnapshotRef; |
29 | 30 | import org.apache.iceberg.TableMetadata; |
30 | 31 | import org.apache.iceberg.TableOperations; |
31 | 32 | import org.apache.iceberg.TableProperties; |
32 | 33 | import org.apache.iceberg.UpdateRequirement; |
33 | 34 | import org.apache.iceberg.UpdateRequirements; |
34 | 35 | import org.apache.iceberg.encryption.EncryptionManager; |
| 36 | +import org.apache.iceberg.exceptions.CommitStateUnknownException; |
35 | 37 | import org.apache.iceberg.io.FileIO; |
36 | 38 | import org.apache.iceberg.io.LocationProvider; |
37 | 39 | import org.apache.iceberg.relocated.com.google.common.base.Preconditions; |
@@ -155,20 +157,90 @@ public void commit(TableMetadata base, TableMetadata metadata) { |
155 | 157 | // the error handler will throw necessary exceptions like CommitFailedException and |
156 | 158 | // UnknownCommitStateException |
157 | 159 | // TODO: ensure that the HTTP client lib passes HTTP client errors to the error handler |
158 | | - LoadTableResponse response = |
159 | | - client.post(path, request, LoadTableResponse.class, headers, errorHandler); |
| 160 | + LoadTableResponse response; |
| 161 | + try { |
| 162 | + response = client.post(path, request, LoadTableResponse.class, headers, errorHandler); |
| 163 | + } catch (CommitStateUnknownException e) { |
| 164 | + // Lightweight reconciliation for snapshot-add-only updates on transient unknown commit state |
| 165 | + if (updateType == UpdateType.SIMPLE && reconcileOnSimpleUpdate(updates, e)) { |
| 166 | + return; |
| 167 | + } |
| 168 | + |
| 169 | + throw e; |
| 170 | + } |
160 | 171 |
|
161 | 172 | // all future commits should be simple commits |
162 | 173 | this.updateType = UpdateType.SIMPLE; |
163 | 174 |
|
164 | 175 | updateCurrentMetadata(response); |
165 | 176 | } |
166 | 177 |
|
| 178 | + /** |
| 179 | + * Attempt best-effort reconciliation for SIMPLE updates that only add a snapshot. |
| 180 | + * |
| 181 | + * <p>Returns true if the expected snapshot is observed in the refreshed table state. Returns |
| 182 | + * false if the expected snapshot cannot be determined, is not present after refresh, or if the |
| 183 | + * refresh fails. In case of refresh failure, the failure is recorded as suppressed on the |
| 184 | + * provided {@code original} exception to aid diagnostics. |
| 185 | + */ |
| 186 | + private boolean reconcileOnSimpleUpdate( |
| 187 | + List<MetadataUpdate> updates, CommitStateUnknownException original) { |
| 188 | + Long expectedSnapshotId = expectedSnapshotIdIfSnapshotAddOnly(updates); |
| 189 | + if (expectedSnapshotId == null) { |
| 190 | + return false; |
| 191 | + } |
| 192 | + |
| 193 | + try { |
| 194 | + TableMetadata refreshed = refresh(); |
| 195 | + return refreshed != null && refreshed.snapshot(expectedSnapshotId) != null; |
| 196 | + } catch (RuntimeException reconEx) { |
| 197 | + original.addSuppressed(reconEx); |
| 198 | + return false; |
| 199 | + } |
| 200 | + } |
| 201 | + |
167 | 202 | @Override |
168 | 203 | public FileIO io() { |
169 | 204 | return io; |
170 | 205 | } |
171 | 206 |
|
| 207 | + private static Long expectedSnapshotIdIfSnapshotAddOnly(List<MetadataUpdate> updates) { |
| 208 | + Long addedSnapshotId = null; |
| 209 | + Long mainRefSnapshotId = null; |
| 210 | + |
| 211 | + for (MetadataUpdate update : updates) { |
| 212 | + if (update instanceof MetadataUpdate.AddSnapshot) { |
| 213 | + if (addedSnapshotId != null) { |
| 214 | + return null; // multiple snapshot adds -> not safe |
| 215 | + } |
| 216 | + addedSnapshotId = ((MetadataUpdate.AddSnapshot) update).snapshot().snapshotId(); |
| 217 | + } else if (update instanceof MetadataUpdate.SetSnapshotRef) { |
| 218 | + MetadataUpdate.SetSnapshotRef setRef = (MetadataUpdate.SetSnapshotRef) update; |
| 219 | + if (!SnapshotRef.MAIN_BRANCH.equals(setRef.name())) { |
| 220 | + return null; // only allow main ref update |
| 221 | + } |
| 222 | + mainRefSnapshotId = setRef.snapshotId(); |
| 223 | + } else { |
| 224 | + // any other update type makes this not a pure snapshot-add |
| 225 | + return null; |
| 226 | + } |
| 227 | + } |
| 228 | + |
| 229 | + if (addedSnapshotId == null) { |
| 230 | + return null; |
| 231 | + } |
| 232 | + |
| 233 | + if (mainRefSnapshotId != null && !addedSnapshotId.equals(mainRefSnapshotId)) { |
| 234 | + // Only handle "append to main" here. In this request, main is being set to a snapshot ID |
| 235 | + // that is different from the snapshot we just added (e.g., rollback or move main elsewhere). |
| 236 | + // In that case, finding the added snapshot in history doesn't tell us whether main moved to |
| 237 | + // it, so skip reconciliation. |
| 238 | + return null; |
| 239 | + } |
| 240 | + |
| 241 | + return addedSnapshotId; |
| 242 | + } |
| 243 | + |
172 | 244 | private TableMetadata updateCurrentMetadata(LoadTableResponse response) { |
173 | 245 | // LoadTableResponse is used to deserialize the response, but config is not allowed by the REST |
174 | 246 | // spec so it can be |
|
0 commit comments