Skip to content

Conversation

@snazy
Copy link
Member

@snazy snazy commented Oct 8, 2021

  • Use new NessieApiV1
  • Update reference syntax to table-identifier ( '@' reference-name )? ( '#' pointer )? (for Nessie-Iceberg-GC)
  • Slightly nicer commit message from NessieTableOperations

Contributes to #3243

/cc @nastra

@snazy
Copy link
Member Author

snazy commented Oct 10, 2021

Moved to draft status due to projectnessie/nessie#2312

@rdblue
Copy link
Contributor

rdblue commented Oct 10, 2021

@nastra, can you take a look?

@rdblue
Copy link
Contributor

rdblue commented Oct 10, 2021

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?

@snazy snazy force-pushed the tvs2 branch 3 times, most recently from a636324 to cc353e1 Compare October 13, 2021 13:31
@snazy
Copy link
Member Author

snazy commented Oct 14, 2021

@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".

@snazy snazy force-pushed the tvs2 branch 5 times, most recently from b22785c to aed18cf Compare October 20, 2021 12:48
@snazy snazy changed the title Bump Nessie to 0.10.1 + related changes Bump Nessie to 0.11.0 + related changes Oct 20, 2021
@snazy snazy marked this pull request as ready for review October 20, 2021 13:35
@snazy snazy force-pushed the tvs2 branch 2 times, most recently from 2b13c6a to 4276e63 Compare October 20, 2021 13:44
@jackye1995
Copy link
Contributor

jackye1995 commented Oct 23, 2021

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 A that added column c1 as an int column, branch B also added column c1 as a string column, even if they have different ID, it is unclear if the user's intention is to keep one c1 (and we don't know which one to keep), or keep both (which is not possible due to duplicated column name).

@snazy
Copy link
Member Author

snazy commented Oct 23, 2021

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 A that added column c1 as an int column, branch B also added column c1 as a string column, even if they have different ID, it is unclear if the user's intention is to keep one c1 (and we don't know which one to keep), or keep both (which is not possible due to duplicated column name).

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.

Copy link
Contributor

@harshm-dev harshm-dev left a 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 {
Copy link
Contributor

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);
Copy link
Contributor

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,
Copy link
Contributor

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);
Copy link
Contributor

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;
Copy link
Contributor

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) {
Copy link
Contributor

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.

Copy link
Contributor

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

Copy link
Contributor

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()) {
Copy link
Contributor

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(..)

Copy link
Contributor

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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

debug level please

Copy link
Contributor

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);
Copy link
Contributor

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);

Copy link
Contributor

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

Copy link
Contributor

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) {
Copy link
Member

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.

Copy link
Contributor

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) {
Copy link
Member

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

Copy link
Contributor

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()
Copy link
Member

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.

Copy link
Contributor

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 -> {
Copy link
Member

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?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

@snazy snazy force-pushed the tvs2 branch 2 times, most recently from ff67abc to 486a297 Compare November 4, 2021 09:43
@snazy snazy changed the title Bump Nessie to 0.12.0 + related changes Bump Nessie to 0.14.0 + related changes Nov 12, 2021
Copy link
Contributor

@nastra nastra left a 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()) {
Copy link
Contributor

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()
Copy link
Contributor

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 -> {
Copy link
Contributor

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);
Copy link
Contributor

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) {
Copy link
Contributor

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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Copy link
Contributor

@rymurr rymurr left a 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"
Copy link
Contributor

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) {
Copy link
Contributor

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) {
Copy link
Contributor

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.

Copy link
Contributor

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?

Copy link
Contributor

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() {
Copy link
Contributor

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);
Copy link
Contributor

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

@jackye1995 jackye1995 added this to the Iceberg 0.13.0 Release milestone Nov 19, 2021
@snazy snazy force-pushed the tvs2 branch 4 times, most recently from 4e62dc1 to 081b130 Compare November 23, 2021 10:06
@snazy snazy force-pushed the tvs2 branch 2 times, most recently from 9b2de70 to 98ad99a Compare December 1, 2021 14:46
@snazy snazy changed the title Bump Nessie to 0.14.0 + related changes Bump Nessie to 0.15.1 + related changes Dec 1, 2021
@snazy snazy force-pushed the tvs2 branch 2 times, most recently from d17fe25 to 85f5085 Compare December 1, 2021 16:16
Copy link
Contributor

@rymurr rymurr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM thanks @snazy and @nastra

* 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>
@rymurr rymurr merged commit 699bdd1 into apache:master Dec 2, 2021
@snazy snazy deleted the tvs2 branch December 2, 2021 18:06
.shouldRetryTest(shouldRetry)
.run(metadataLocation -> newMetadata.set(
TableMetadataParser.read(io(), metadataLocation)));
.run(metadataLocation -> newMetadata.set(metadataLoader.apply(metadataLocation)));
Copy link
Contributor

@rdblue rdblue Dec 4, 2021

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);
  }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants