-
Notifications
You must be signed in to change notification settings - Fork 3k
Bump Nessie to 0.15.1 + related changes #3257
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
9fc9e83 to
ae35eef
Compare
|
Moved to draft status due to projectnessie/nessie#2312 |
|
@nastra, can you take a look? |
core/src/main/java/org/apache/iceberg/BaseMetastoreTableOperations.java
Outdated
Show resolved
Hide resolved
|
This appears to change the relationship between Nessie and Iceberg metadata. Can you please explain what you're doing here and why it is needed? What is the model for Nessie and Iceberg tracking now? I thought it was that Nessie tracked different Iceberg metadata files. Is that no longer the case and Nessie is tracking structures within Iceberg? |
a636324 to
cc353e1
Compare
|
@rdblue Sorry for the late reply. Yes, this one changes the relationship between Nessie and Iceberg metadata. TL;DR the changes shall ensure that changes against the same table on different branches can later be merged together without having duplicate column-IDs or partition-IDs or the like. You're right, initially (in the "early Nessie days"), every Nessie commit held a pointer to the table-metadata. This works fine until you reference the same table on different branches and perform e.g. schema changes (think: ALTER TABLE ADD COLUMN) on both branches, which leads to duplicate column-ids (in other words: same column id used for different columns), which then can lead to data corruption when branches get merged. So the initial approach in this PR was to maintain table-metadata across all branches and "just" reference the snapshot-ID from in Nessie commits, which led to other issues (not explaining it here further, but schema changes became an issue again). The current approach is more like the initial approach: have the pointer to table-metadata in Nessie commits but track state that's important across all Nessie branches (e.g. last-column-ID) globally. It's currently implemented via additional functionality in TableMetadata to retrieve the "global state" (last-column-ID, last-used-partition-ID, last-assigned-sequence-ID) as an object that's opaque to Nessie plus functionality to update a TableMetadata using that "global state". |
b22785c to
aed18cf
Compare
2b13c6a to
4276e63
Compare
|
It's still unclear to me how this would solve the merge conflict if you have 2 columns updated in different branches. For example, if I have branch |
Yes, that’s a known concern. It would have to lead in an merge conflict error. However, it could be resolved by the user by renaming one of those. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for posting this change.
| * of the relevant ID generators across all references. This allows cherry-picking and merging | ||
| * specific changes onto different branches. | ||
| */ | ||
| public final class TableIdGenerators { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: The class isn't really generating any IDs, just holding the state instead. Using the suffix "IdGenerators" is confusing.
| public static String toJson(TableIdGenerators idGenerators) { | ||
| try (StringWriter writer = new StringWriter()) { | ||
| JsonGenerator generator = JsonUtil.factory().createGenerator(writer); | ||
| idGeneratorsJson(idGenerators, generator); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not just directly json serialise/de-serialise the object?
PropertyNames/validations can be achieved via appropriate annotations.
| } | ||
|
|
||
| // intentionally package-private - use TableMetadata.getIdGenerators() or parse via TableIdGeneratorsParser | ||
| static TableIdGenerators of(int formatVersion, int lastColumnId, int lastAssignedPartitionId, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just wondering why constructor cannot be invoked directly.
What's the intention behind using a factory method?
| metadataLocation = table.getMetadataLocation(); | ||
| Contents contents = api.getContents().key(key).reference(reference.getReference()).get() | ||
| .get(key); | ||
| LOG.info("Contents '{}' at '{}': {}", key, reference.getReference(), contents); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be a debug level log.
| private static final Logger logger = LoggerFactory.getLogger(NessieCatalog.class); | ||
| private static final Joiner SLASH = Joiner.on("/"); | ||
| private NessieClient client; | ||
| private NessieApiV1 api; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
client is relatively a better variable name at the consumption. clientApi could be an option since the class name is also changing.
| this.shouldRefresh = false; | ||
| } | ||
|
|
||
| protected TableMetadata validateRefreshedMetadata(TableMetadata metadata) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From the code below, the method seems to be supplementing metadata with global state IDs and returning a mutated object.
Naming this method as validateXX doesn't feel correct.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agreed. I would go further and say this additional protected method feels fragile and not a great use of class hierarchies. I would prefer NessieTableOperations just reimplements the above refreshFromMetadataLocation completely rather than adding this shim
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that would require making a bunch of fields protected in the base class. I renamed the method to maybeValidateAndUpdateMetadata. Hopefully that makes its purpose clearer
| protected TableOperations newTableOps(TableIdentifier tableIdentifier) { | ||
| TableReference pti = TableReference.parse(tableIdentifier); | ||
| TableReference tr = TableReference.parse(tableIdentifier.name()); | ||
| if (tr.hasTimestamp()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Better to use Precoditions.checkArgument(..)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
| .commitMeta(NessieUtil.buildCommitMetadata("iceberg commit", catalogOptions)).build(); | ||
| Branch branch = client.getTreeApi().commitMultipleOperations(reference.getAsBranch().getName(), | ||
| reference.getHash(), op); | ||
| LOG.info("Committing '{}' against '{}': {}", key, reference.getReference(), newTable); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
debug level please
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
| commitMessage = String.format("Iceberg commit against %s", tableName()); | ||
| } | ||
|
|
||
| return commitMeta.message(commitMessage); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is useful, thx!
However, the function is taking multiple responsibilities - initialising a builder, setting properties apart from identifying commit message.
I think we should separate them out, an example version of the consuming side of the code -
ImmutableCommitMeta.Builder commitMetaBuilder = ImmutableCommitMeta.builder();
addOperationDetails(commitMetaBuilder, base, metadata); // this method
NessieUtils.addCatalogOptions(commitMetaBuilder, catalogOptions);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also find this rather confusing to read, some rather complicated booleans floating around. This seems like complexity in the name of removing code duplication
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
improved
| } | ||
|
|
||
| public void updateReference(Reference ref) { | ||
| if (!mutable) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think Preconditions.checkState(mutable, "Hash references cannot be updated.") would be nicer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
| public void checkMutable() { | ||
| if (!isBranch()) { | ||
| throw new IllegalArgumentException("You can only mutate tables when using a branch."); | ||
| if (!mutable) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here is as well, perhaps Preconditions.checkArgument would be nicer
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
| .addOperations( | ||
| ImmutablePut.builder().key(NessieUtil.toKey(to)).contents(existingFromTable).build(), | ||
| ImmutableDelete.builder().key(NessieUtil.toKey(from)).build()) | ||
| CommitMultipleOperationsBuilder op = api.commitMultipleOperations() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Can we change op to something plural like operations to have more readable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
| Branch branch = client.getTreeApi().commitMultipleOperations(reference.getAsBranch().getName(), | ||
| reference.getHash(), c); | ||
| .onFailure((o, exception) -> refresh()) | ||
| .run(o -> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: perhaps singular operation would make sense instead of o here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
ff67abc to
486a297
Compare
nastra
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since Robert is out this week, I made some changes according to the review feedback
| protected TableOperations newTableOps(TableIdentifier tableIdentifier) { | ||
| TableReference pti = TableReference.parse(tableIdentifier); | ||
| TableReference tr = TableReference.parse(tableIdentifier.name()); | ||
| if (tr.hasTimestamp()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
| .addOperations( | ||
| ImmutablePut.builder().key(NessieUtil.toKey(to)).contents(existingFromTable).build(), | ||
| ImmutableDelete.builder().key(NessieUtil.toKey(from)).build()) | ||
| CommitMultipleOperationsBuilder op = api.commitMultipleOperations() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
| Branch branch = client.getTreeApi().commitMultipleOperations(reference.getAsBranch().getName(), | ||
| reference.getHash(), c); | ||
| .onFailure((o, exception) -> refresh()) | ||
| .run(o -> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
| .commitMeta(NessieUtil.buildCommitMetadata("iceberg commit", catalogOptions)).build(); | ||
| Branch branch = client.getTreeApi().commitMultipleOperations(reference.getAsBranch().getName(), | ||
| reference.getHash(), op); | ||
| LOG.info("Committing '{}' against '{}': {}", key, reference.getReference(), newTable); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
| } | ||
|
|
||
| public void updateReference(Reference ref) { | ||
| if (!mutable) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
| public void checkMutable() { | ||
| if (!isBranch()) { | ||
| throw new IllegalArgumentException("You can only mutate tables when using a branch."); | ||
| if (!mutable) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
rymurr
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added some comments, I think these changes could be architected better.
Also: this is a fairly fundamental change to how nessie and iceberg interact. I would expect more explanation of the change and its motivation and probably some docs changes (or links to nessie docs explaining iceberg/nessie interaction)
build.gradle
Outdated
| implementation project(':iceberg-core') | ||
| implementation project(path: ':iceberg-bundled-guava', configuration: 'shadow') | ||
| implementation "org.projectnessie:nessie-client" | ||
| implementation "com.fasterxml.jackson.core:jackson-databind" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this a new dependency or just being explicit about an existing one? If new does this change the size of the runtime jars?
| this.shouldRefresh = false; | ||
| } | ||
|
|
||
| protected TableMetadata validateRefreshedMetadata(TableMetadata metadata) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agreed. I would go further and say this additional protected method feels fragile and not a great use of class hierarchies. I would prefer NessieTableOperations just reimplements the above refreshFromMetadataLocation completely rather than adding this shim
| snapshots, newSnapshotLog, addPreviousFile(file, lastUpdatedMillis)); | ||
| } | ||
|
|
||
| public TableMetadata withUpdatedSnapshotAndSchema(long snapshotId, int schemaId, int sortOrderId, int specId) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think these would be more useful as separate methods. There is no way to only update snapshotId for example and all 4 args have to be given valid values or it throws. Seems overly restrictive and not very composable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that things would be easier with the changes from #2957, where we could just call the builder and only update the actual fields that we need. Then we wouldn't need to have the withUpdatedSnapshotAndSchema method in TableMetadata and could do everything in the NessieCatalog where we need it. Maybe we should go forward with this as is right now and then refactor/improve this once #2957 is merged?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For #2957 I was waiting for more clarity around the REST catalog to design the table metadata update builder semantics, so it probably won't be merged very soon. But I agree with Ryan that this seems like a very restrictive method, and the intention of the operation is unclear comparing to the other update methods (although I know how it fits in the Nessie context).
To update the schemaId, sortOrderId, specId to a specific version, I think it makes more sense to expose specific APIs in related update classes instead of having everything done through a separated method. What do you think?
|
|
||
| TreeApi getTreeApi() { | ||
| return client.getTreeApi(); | ||
| public NessieApiV1 getApi() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why is this public? I don't think we should be exposing this
| commitMessage = String.format("Iceberg commit against %s", tableName()); | ||
| } | ||
|
|
||
| return commitMeta.message(commitMessage); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also find this rather confusing to read, some rather complicated booleans floating around. This seems like complexity in the name of removing code duplication
4e62dc1 to
081b130
Compare
core/src/main/java/org/apache/iceberg/BaseMetastoreTableOperations.java
Outdated
Show resolved
Hide resolved
9b2de70 to
98ad99a
Compare
d17fe25 to
85f5085
Compare
rymurr
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* Use new NessieApiV1 * Update reference syntax to `table-identifier ( '@' reference-name )? ( '#' pointer )?` (for Nessie-Iceberg-GC) * Slightly nicer commit message from `NessieTableOperations` * Use new `TableIdGenerators` as the "global state" tracked in Nessie
Co-authored-by: Ajantha Bhat <ajanthabhat@gmail.com>
| .shouldRetryTest(shouldRetry) | ||
| .run(metadataLocation -> newMetadata.set( | ||
| TableMetadataParser.read(io(), metadataLocation))); | ||
| .run(metadataLocation -> newMetadata.set(metadataLoader.apply(metadataLocation))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rymurr, I'm just catching up on this PR, sorry to be late.
I'm curious why this uses a Function passed into refresh rather than defining a load metadata method. The function passed in from NessieTableOperations is actually a method reference, so couldn't this just be a call to that method, loadTableMetadata?
.run(metadataLocation -> loadTableMetadata(metadataLocation));
...
public TableMetadata loadTableMetadata(String metadataLocation) {
return TableMetadataParser.read(io(), metadataLocation);
}
table-identifier ( '@' reference-name )? ( '#' pointer )?(for Nessie-Iceberg-GC)NessieTableOperationsContributes to #3243
/cc @nastra