Skip to content

Commit d1fb14a

Browse files
committed
Finer-grained checks at branch level
1 parent b802a29 commit d1fb14a

File tree

6 files changed

+299
-44
lines changed

6 files changed

+299
-44
lines changed

core/src/main/java/org/apache/iceberg/BaseScan.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ protected Schema tableSchema() {
110110
return schema;
111111
}
112112

113-
protected TableScanContext context() {
113+
public TableScanContext context() {
114114
return context;
115115
}
116116

api/src/main/java/org/apache/iceberg/catalog/CatalogTransaction.java renamed to core/src/main/java/org/apache/iceberg/catalog/CatalogTransaction.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,6 @@ enum IsolationLevel {
6666
*/
6767
void commitTransaction();
6868

69-
/** Rolls back any pending changes across tables. */
70-
void rollback();
71-
7269
/**
7370
* Returns this catalog transaction as a {@link Catalog} API so that any actions that are called
7471
* through this API are participating in this catalog transaction.

core/src/main/java/org/apache/iceberg/rest/RESTCatalog.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -255,9 +255,7 @@ public void setConf(Object conf) {
255255
sessionCatalog.setConf(conf);
256256
}
257257

258-
/**
259-
* @deprecated will be removed in 1.3.0; use {@link #setConf(Object)}
260-
*/
258+
/** @deprecated will be removed in 1.3.0; use {@link #setConf(Object)} */
261259
@Deprecated
262260
public void setConf(Configuration conf) {
263261
setConf((Object) conf);

core/src/main/java/org/apache/iceberg/rest/RESTCatalogTransaction.java

Lines changed: 97 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.apache.iceberg.AppendFiles;
2626
import org.apache.iceberg.BaseTable;
2727
import org.apache.iceberg.BaseTransaction;
28+
import org.apache.iceberg.DataTableScan;
2829
import org.apache.iceberg.DeleteFiles;
2930
import org.apache.iceberg.ExpireSnapshots;
3031
import org.apache.iceberg.HasTableOperations;
@@ -37,10 +38,12 @@
3738
import org.apache.iceberg.RewriteFiles;
3839
import org.apache.iceberg.RewriteManifests;
3940
import org.apache.iceberg.RowDelta;
41+
import org.apache.iceberg.SnapshotRef;
4042
import org.apache.iceberg.Table;
4143
import org.apache.iceberg.TableMetadata;
4244
import org.apache.iceberg.TableMetadataDiffAccess;
4345
import org.apache.iceberg.TableOperations;
46+
import org.apache.iceberg.TableScan;
4447
import org.apache.iceberg.Transaction;
4548
import org.apache.iceberg.Transactions;
4649
import org.apache.iceberg.UpdateLocation;
@@ -59,10 +62,11 @@
5962
import org.apache.iceberg.relocated.com.google.common.collect.Maps;
6063
import org.apache.iceberg.rest.requests.UpdateTableRequest;
6164
import org.apache.iceberg.util.Tasks;
65+
import org.immutables.value.Value;
6266

6367
public class RESTCatalogTransaction implements CatalogTransaction {
6468
private final Map<TableIdentifier, Transaction> txByTable;
65-
private final Map<TableIdentifier, TableMetadata> initiallyReadTableMetadata;
69+
private final Map<TableRef, TableMetadata> initiallyReadTableMetadataByRef;
6670
private final Map<TableIdentifier, Table> initiallyReadTables;
6771
private final IsolationLevel isolationLevel;
6872
private final Catalog origin;
@@ -83,17 +87,17 @@ public RESTCatalogTransaction(
8387
this.isolationLevel = isolationLevel;
8488
this.sessionCatalog = sessionCatalog;
8589
this.context = context;
86-
this.txByTable = Maps.newConcurrentMap();
87-
this.initiallyReadTableMetadata = Maps.newConcurrentMap();
88-
this.initiallyReadTables = Maps.newConcurrentMap();
90+
this.txByTable = Maps.newHashMap();
91+
this.initiallyReadTableMetadataByRef = Maps.newHashMap();
92+
this.initiallyReadTables = Maps.newHashMap();
8993
}
9094

9195
@Override
9296
public void commitTransaction() {
9397
Preconditions.checkState(!hasCommitted, "Transaction has already committed changes");
9498

9599
try {
96-
Map<TableIdentifier, List<PendingUpdate>> pendingUpdatesByTable = Maps.newConcurrentMap();
100+
Map<TableIdentifier, List<PendingUpdate>> pendingUpdatesByTable = Maps.newHashMap();
97101
txByTable.forEach((key, value) -> pendingUpdatesByTable.put(key, value.pendingUpdates()));
98102
Map<TableIdentifier, UpdateTableRequest> updatesByTable = Maps.newHashMap();
99103

@@ -153,33 +157,48 @@ public void commitTransaction() {
153157
* happens due to a transaction taking action based on an outdated premise (a fact that was true
154158
* when a table was initially loaded but then changed due to a concurrent update to the table
155159
* while this TX was in-progress). When this TX wants to commit, the original premise might not
156-
* hold anymore, thus we need to check whether {@link TableMetadata} changed after it was
157-
* initially read inside this TX.
160+
* hold anymore, thus we need to check whether the {@link org.apache.iceberg.Snapshot} a branch
161+
* was pointing to changed after it was initially read inside this TX. If no information a
162+
* branch's snapshot is available, we check whether {@link TableMetadata} changed after it was
163+
* initially read.
158164
*/
159165
private void validateSerializableIsolation() {
160-
for (TableIdentifier readTable : initiallyReadTableMetadata.keySet()) {
166+
for (TableRef readTable : initiallyReadTableMetadataByRef.keySet()) {
161167
// we need to check all read tables to determine whether they changed outside the catalog
162-
// TX after we initially read them
168+
// TX after we initially read them on a particular branch
163169
if (IsolationLevel.SERIALIZABLE == isolationLevel) {
164-
TableMetadata currentTableMetadata =
165-
((BaseTable) origin.loadTable(readTable)).operations().current();
166-
167-
if (!currentTableMetadata
168-
.metadataFileLocation()
169-
.equals(initiallyReadTableMetadata.get(readTable).metadataFileLocation())) {
170+
BaseTable table = (BaseTable) origin.loadTable(readTable.identifier());
171+
SnapshotRef snapshotRef = table.operations().current().ref(readTable.ref());
172+
SnapshotRef snapshotRefInsideTx =
173+
initiallyReadTableMetadataByRef.get(readTable).ref(readTable.ref());
174+
175+
if (null != snapshotRef
176+
&& null != snapshotRefInsideTx
177+
&& snapshotRef.snapshotId() != snapshotRefInsideTx.snapshotId()) {
170178
throw new ValidationException(
171-
"%s isolation violation: Found table metadata updates to table '%s' after it was read",
172-
isolationLevel(), readTable);
179+
"%s isolation violation: Found table metadata updates to table '%s' after it was read on branch '%s'",
180+
isolationLevel(), readTable.identifier().toString(), readTable.ref());
181+
}
182+
183+
if (null == snapshotRef || null == snapshotRefInsideTx) {
184+
TableMetadata currentTableMetadata = table.operations().current();
185+
186+
if (!currentTableMetadata
187+
.metadataFileLocation()
188+
.equals(initiallyReadTableMetadataByRef.get(readTable).metadataFileLocation())) {
189+
throw new ValidationException(
190+
"%s isolation violation: Found table metadata updates to table '%s' after it was read",
191+
isolationLevel(), readTable.identifier());
192+
}
173193
}
174194
}
175195
}
176196
}
177197

178-
@Override
179-
public void rollback() {
198+
private void rollback() {
180199
Tasks.foreach(txByTable.values()).run(Transaction::rollback);
181200
txByTable.clear();
182-
initiallyReadTableMetadata.clear();
201+
initiallyReadTableMetadataByRef.clear();
183202
initiallyReadTables.clear();
184203
}
185204

@@ -246,19 +265,18 @@ public Table loadTable(TableIdentifier identifier) {
246265
// we need to remember the very first version of table metadata that
247266
// we read
248267
if (IsolationLevel.SERIALIZABLE == isolationLevel()) {
249-
initiallyReadTableMetadata.computeIfAbsent(
250-
identifier,
251-
ident -> ((BaseTable) loadTable).operations().current());
268+
initiallyReadTableMetadataByRef.computeIfAbsent(
269+
ImmutableTableRef.builder()
270+
.identifier(identifier)
271+
.ref(SnapshotRef.MAIN_BRANCH)
272+
.build(),
273+
ident -> opsFromTable(loadTable).current());
252274
}
253275

254276
return loadTable;
255277
}));
256278

257-
TableOperations tableOps =
258-
table instanceof BaseTransaction.TransactionTable
259-
? ((BaseTransaction.TransactionTable) table).operations()
260-
: ((BaseTable) table).operations();
261-
return new TransactionalTable(table, tableOps);
279+
return new TransactionalTable(table, opsFromTable(table));
262280
}
263281

264282
@Override
@@ -277,6 +295,12 @@ public void renameTable(TableIdentifier from, TableIdentifier to) {
277295
}
278296
}
279297

298+
private static TableOperations opsFromTable(Table table) {
299+
return table instanceof BaseTransaction.TransactionTable
300+
? ((BaseTransaction.TransactionTable) table).operations()
301+
: ((BaseTable) table).operations();
302+
}
303+
280304
private class TransactionalTable extends BaseTable {
281305
private final Table table;
282306

@@ -285,6 +309,16 @@ private TransactionalTable(Table table, TableOperations ops) {
285309
this.table = table;
286310
}
287311

312+
@Override
313+
public TableScan newScan() {
314+
TableScan tableScan = super.newScan();
315+
if (tableScan instanceof DataTableScan) {
316+
return new TransactionalTableScan((DataTableScan) tableScan);
317+
}
318+
319+
return tableScan;
320+
}
321+
288322
@Override
289323
public UpdateSchema updateSchema() {
290324
return txForTable(table).updateSchema();
@@ -365,4 +399,39 @@ public ManageSnapshots manageSnapshots() {
365399
return txForTable(table).manageSnapshots();
366400
}
367401
}
402+
403+
private class TransactionalTableScan extends DataTableScan {
404+
protected TransactionalTableScan(DataTableScan delegate) {
405+
super(delegate.table(), delegate.schema(), delegate.context());
406+
}
407+
408+
@Override
409+
public TableScan useRef(String name) {
410+
DataTableScan tableScan = (DataTableScan) super.useRef(name);
411+
412+
if (IsolationLevel.SERIALIZABLE == isolationLevel()) {
413+
// store which version of the table on the given branch we read the first time
414+
initiallyReadTableMetadataByRef.computeIfAbsent(
415+
ImmutableTableRef.builder()
416+
.identifier(identifierWithoutCatalog(table().name()))
417+
.ref(name)
418+
.build(),
419+
ident -> opsFromTable(table()).current());
420+
}
421+
422+
return tableScan;
423+
}
424+
}
425+
426+
@Value.Immutable
427+
interface TableRef {
428+
TableIdentifier identifier();
429+
430+
String ref();
431+
432+
@Value.Lazy
433+
default String name() {
434+
return identifier().toString() + "@" + ref();
435+
}
436+
}
368437
}

0 commit comments

Comments
 (0)