-
Notifications
You must be signed in to change notification settings - Fork 2.3k
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
API: Support removeUnusedSpecs in ExpireSnapshots #10755
base: main
Are you sure you want to change the base?
Conversation
@amogh-jahagirdar @RussellSpitzer @aokolnychyi @szehon-ho it would be great if you guys could take a look at this. This PR is raised based the previous discussion in #10352 (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.
Thank you for carrying this forward @advancedxy ! I don't think I'd go with a general SetPartitionSpecs
update, I think I'd have a RemovePartitionSpec
, and the TableMetadata builder APIs to remove a given spec (which will have validation that we're not removing the current spec, the spec to remove is a valid partition spec etc).
Few more things to consider:
1.) Should we included removing unused schemas? I know there are users whose tables undergo numerous evolutions for adding fields, and they want to remove old schemas since their metadata ends up being so bloated to the point of performance concerns when reading! My conclusion here is no, I believe that should be a separate operation since I think we want APIs to do one thing and do it well.. A caller can combine the two if desired.
- In this approach I think we'd have to do a REST spec change to introduce the new update type. If we think this API is worth it for general purpose metadata cleaning, beyond preventing the drop column issue then we'd probably have to go through with the spec change. However, if this is only being introduced for the drop column issue, maybe we want to think through lighter weight options to solve that particular issue?
Another possible way: are we able to retain the spec and essentially not care about the dropped field's existence? This may be something worth exploring. Sorry for failing to considering this earlier, I wasn't considering this because I assumed we wouldn't need any heavy weight spec changes and the API would be generalizable beyond this drop column case. I'll also do some exploration here.
This is a nice suggestion and better approach. I will change the Replying other comments inline.
No, and I think we are on the same page.
I don't think we should expose If introducing a new MetadataUpdate implies a REST spec change, I think we can change the |
toBeRemoved != null && toBeRemoved.equals(spec), | ||
"Cannot remove an unknown spec, spec id: %s", | ||
spec.specId()); | ||
this.specs = |
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.
Should we be validating that all the specs we are trying to remove exist? I think this may be fine, just thinking about commit conflicts
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.
Should we be validating that all the specs we are trying to remove exist?
I think so, and L1129 - L1133 indeed ensures the spec to be removed are existed in current table metadata.
I think this may be fine, just thinking about commit conflicts
If you are talking about concurrent RemovedUnusedSpecs
, I think it's fine and reasonable to have only one attempt succeeded once at time. For other concurrent commit, there's no conflict and retrying should do the trick.
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.
In general I'm in favor of succeeding in cases like this. There's no need to fail something if it has an accidental retry after the first attempt succeeded.
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.
There's no need to fail something if it has an accidental retry after the first attempt succeeded.
This is a very good point, let me reconsider this part.
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.
Addressed in 190dde6, removing an already removed unused spec should not cause a failure then.
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.
In that case, is it really necessary to validate that a spec was removed and filter the list? If it is a noop to remove an unknown spec ID, then we can simplify the first half of this method:
Preconditions.checkArgument(!specIds.contains(defaultSpecId), "Cannot remove the default partition spec");
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.
Updated, let me know if this is the desired change.
Close and re-open to trigger the CI. Also gently ping @amogh-jahagirdar @RussellSpitzer to take another look. |
core/src/main/java/org/apache/iceberg/BaseRemoveUnusedSpecs.java
Outdated
Show resolved
Hide resolved
core/src/main/java/org/apache/iceberg/BaseRemoveUnusedSpecs.java
Outdated
Show resolved
Hide resolved
/** | ||
* Prune the unused partition specs from the table metadata. | ||
* | ||
* <p>Note: it's not safe for external client to call this directly, it's usually called by the |
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 method is package private, does it need this note?
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'd like to highlight this note to reduce potential misusage as it's possible for users to put their own code in org.apache.iceberg
package to bypass Java's access control.
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 agree with @RussellSpitzer. This class is internal and many of its methods can break tables if called incorrectly.
Also, we are no longer adding new operation methods to this. These days we make modifications to the Builder
instead.
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.
Let me remove the note then.
Also, we are no longer adding new operation methods to this. These days we make modifications to the Builder instead.
For this part, I think modifications are still going through the Builders. The main purpose of this method is to provide a single access point to prune unused specs purely so that prune unused specs are not mixed with other metadata updates.
core/src/test/java/org/apache/iceberg/TestRemoveUnusedSpecs.java
Outdated
Show resolved
Hide resolved
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 this is really close, I just want to have those remaining nits of mine addressed.
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.
@advancedxy This is getting closer, thank you for moving from a "set" to a "remove" semantic, that fits better with the general principles of the project but I think the main question I had was around why we need a special flag for hasRemovedSpecs
when building? It seems like that's just working around the fact that there's no metadata update, which I know I mentioned would require a spec change for REST. If that's the case, i think the right solution there should be to add a ToDo with a follow on issue for supporting it for REST.
Also since there's no update type for REST this should mean that the operation fails for REST on the client side so it's clear. Again I think that's fine in the interim, until we add the support for it, but I think we should ideally validate that behavior in a test since I think a bad case would be if the commit for the RemoveUnusedSpecs
appears to succeed on REST but nothing actually happens. cc @RussellSpitzer @rdblue
* @param toRemoveSpecs the partition specs to be removed | ||
* @return the new table metadata with the unused partition specs removed | ||
*/ | ||
TableMetadata pruneUnusedSpecs(List<PartitionSpec> toRemoveSpecs) { |
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.
Can we call the parameter specsToRemove
?
core/src/main/java/org/apache/iceberg/BaseRemoveUnusedSpecs.java
Outdated
Show resolved
Hide resolved
core/src/main/java/org/apache/iceberg/BaseRemoveUnusedSpecs.java
Outdated
Show resolved
Hide resolved
if (hasRemovedSpecs) { | ||
Preconditions.checkArgument( | ||
changes.isEmpty(), "Cannot remove partition specs with other metadata update"); |
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.
@advancedxy I'm not sure I follow, what's the intention of this check?
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.
See https://github.com/apache/iceberg/pull/10755/files#r1690420193 and https://github.com/apache/iceberg/pull/10755/files#r1696099907
I think this check is to make sure that RemoveUnusedSpecs
and other metadata updates are not happened together.
@@ -1425,6 +1460,7 @@ private boolean hasChanges() { | |||
|| (discardChanges && !changes.isEmpty()) | |||
|| metadataLocation != null | |||
|| suppressHistoricalSnapshots | |||
|| hasRemovedSpecs |
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.
Hm I'm trying to understand why we need this special flag, is this a way so that we avoid having to add the metadata update type for REST (since then that would ultimately just be in the changes list)?
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 not the right way to update hasChanges
. Instead, this needs to add a change to changes
. That way it is sent to REST services to modify the table in the REST commit path.
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 way so that we avoid having to add the metadata update type for REST (since then that would ultimately just be in the changes list)?
Yes, as discussed earlier, I don't think we should expose removePartitionSpec
directly to the REST API as there's no easy way to ensure that the spec to remove is indeed not used.
This is not the right way to update hasChanges. Instead, this needs to add a change to changes.
It was adding a RemovePartitionSpec
to the changes. However, that would require a REST spec change and it's a bit heavy. See discussions as well: #10755 (comment)
That way it is sent to REST services to modify the table in the REST commit path.
I might be missing something, so all the metadata changes have to be sent to the REST service? I didn't work with a REST catalog before and don't see how changes are sent to the REST service. It would be great that some reference or code could be pointed to.
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 might be missing something, so all the metadata changes have to be sent to the REST service? I didn't work with a REST catalog before and don't see how changes are sent to the REST service. It would be great that some reference or code could be pointed to.
I took a look at the REST catalog related code today, it seems the impl in this PR doesn't work with Iceberg tables backend by REST catalog as there's no update added for RemovePartitionSpec. The RemoveUnusedSpecs
will succeed without actually removing unused specs for REST catalog.
If supporting REST catalog is a must requirement, I think we have to go through a REST spec change to add new update type to reflect that. The only concern is how to enforce the removed spec is indeed not used any more? Do all the similar calculation in org.apache.iceberg.rest.CatalogHandlers#commit
like how we did in BaseRemoveUnusedSpecs
? Or is that necessary?
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.
Yes, as discussed earlier, I don't think we should expose removePartitionSpec directly to the REST API as there's no easy way to ensure that the spec to remove is indeed not used.
We don't currently have operations that can't be supported by the REST protocol, so this is an area where we should be careful. I agree that we want to ensure that there are no new references to specs that are being removed, but I'm skeptical that there is no way to do that. There are also problems with not sending this because it would be a silent no-op when sending changes to REST catalogs.
It's unlikely that a new snapshot would be written with a spec that is being removed because this already validates that the default spec is not being removed. A conflict here would require that a concurrent writer is using a spec other than the default or has changed the default spec. There's already a validation for the second case, assert-default-spec-id
. For the first case, this could require that no branch states have changed using assert-ref-snapshot-id
.
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.
The RemoveUnusedSpecs will succeed without actually removing unused specs for REST catalog.
@advancedxy This indicates there's a problem with the current implementation then imo. If there was a way to avoid the REST spec change I think it would've been OK as long as we can guarantee failure on the client side until the support was added for the metadata update type. But I think that's unavoidable, and I agree with @rdblue that we should probably just add the metadata update type.
I also am reasonably confident that a REST catalog can safely handle this RemovePartitionSpec update. The default spec ID needs to be the same and there must have been no writes to any branches. If any of those are not true, the server should fail the update.
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.
We don't currently have operations that can't be supported by the REST protocol, so this is an area where we should be careful.
Yes, after taking a look at the related code, I think we should strive to make all operations supported by REST protocol.
It's unlikely that a new snapshot would be written with a spec that is being removed because this already validates that the default spec is not being removed. A conflict here would require that a concurrent writer is using a spec other than the default or has changed the default spec. There's already a validation for the second case, assert-default-spec-id. For the first case, this could require that no branch states have changed using assert-ref-snapshot-id.
Thanks for the inspiring explanation. I wasn't worrying about the normal path, which I am also confident that REST catalog can safely handle. I'm more concerned of misusage, such that users issue a RemovePartitionSpec request without going through the RemoveUnusedSpec
API or accidentally call TableMetadata.Builder.removePartitionSpecs
directly. That's why there's method defined in TableMetadata
class in the first place, to hide the Builder's method and ensure single point of access.
For the REST catalog, as it's open by default to all kinds of clients. It's more likely to be affected by the misusage. That's why I'm proposing in the catalog handler side do the check as well:
Do all the similar calculation in org.apache.iceberg.rest.CatalogHandlers#commit like how we did in BaseRemoveUnusedSpecs?
However, it's heavy operation, does that worth it?
core/src/test/java/org/apache/iceberg/TestRemoveUnusedSpecs.java
Outdated
Show resolved
Hide resolved
core/src/test/java/org/apache/iceberg/TestRemoveUnusedSpecs.java
Outdated
Show resolved
Hide resolved
core/src/test/java/org/apache/iceberg/TestRemoveUnusedSpecs.java
Outdated
Show resolved
Hide resolved
core/src/main/java/org/apache/iceberg/BaseRemoveUnusedSpecs.java
Outdated
Show resolved
Hide resolved
* | ||
* <p>{@link #apply()} returns the specs that will remain if committed on the current metadata | ||
*/ | ||
public interface RemoveUnusedSpecs extends PendingUpdate<List<PartitionSpec>> {} |
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.
Does this need to be a separate operation? It seems very specific. I wonder if it is worth adding a maintenance API that could cover more things, like removing old schemas as well.
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.
Missed this comment. I think it would be wonderful to be able to remove unused schemas as well. However, it might depends on #4898 to reliably determine which schemaId is still in use.
Does this need to be a separate operation?
I think so, even if we are going to group other maintenance API like remove unused schema together, they are two different operations and should be in a dedicated operation.
I wonder if it is worth adding a maintenance API that could cover more things, like removing old schemas as well.
Do you by chance have any API name for the Metadata Maintenance
class? I am think about something like MetadataCleaner
.
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.
dedicated operation
By "dedicated operation", I mean a method in the Table
API. Adding this kind of thing is a lot of work, so I'd prefer a reusable option that can handle multiple tasks, like this:
table.maintenance()
.removeUnusedSpecs()
.removeUnusedSchemas()
.commit()
Another option is to do this regularly as part of snapshot expiration. Have you considered that? Since expiration already reads manifests that could be a good place to do this.
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.
[Removing schemas] might depends on #4898 to reliably determine which schemaId is still in use.
We don't need to check whether there are data files that were written with a particular schema ID, only whether there are snapshots that reference the schema ID. Data files are readable by any future schema by design.
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.
table.maintenance()
.removeUnusedSpecs()
.removeUnusedSchemas()
.commit()
This looks promising. But what if we need to further configure the maintenance operations, such as
table.maintenance().removeUnusedSpecs().retainLast(num).commit();
// or something
table.maintenance().removeUnusedSchemas().setMinSchemasToKeep(num).commit();
Different maintenance operations may have different configure options. It would be better to use a dedicated operation for each purpose? Of course, It would be great that these maintenance APIs are grouped together.
Have you considered that? Since expiration already reads manifests that could be a good place to do this.
This is attempting and to be honest I haven't thought about it.
Update: just did a quick look at the RemoveSnapshots' implementation, it might add too much complexity to put remove unused spec logic in there.
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.
@advancedxy I think a decent example to look at would be the ManageSnapshots
API which handles cherry picking/rollback and branching/tagging operations. That is the public interface (analagous to "maintenance" in this case), but the implementation the individual operation implementations are still in separate classes which are package-private and focused on a single operation, as well as to enable different configuration options as you mentioned.
The ManageSnapshots
implementation is tracking all of these operations as part of a transaction (which for the combined schema + partition spec pruning operation case sounds reasonable to me).
I think the pending question is should this be done as part of ExpireSnapshots
or should we have a separate operation. If i think about when it makes sense for this cleanup to happen ExpireSnapshots
does make sense. I also don't know what we'd call a separate "maintenance" API since there's quite a few maintenance operations in Iceberg.
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 a decent example to look at would be the ManageSnapshots API which handles cherry picking/rollback and branching/tagging operations.
Yes, I am thinking about something similar to that.
If i think about when it makes sense for this cleanup to happen ExpireSnapshots does make sense. I also don't know what we'd call a separate "maintenance" API since there's quite a few maintenance operations in Iceberg.
I agree it's attempting. But I would prefer to use a maintenance API, for the following reasons:
- Currently
ExpireSnapshots
extendsPendingUpdate<List<Snapshot>>
, if we are going to remove unused spec(and maybe unused schemas as well), we have to change the interface signature, which is breaking change. - ExpireSnapshots doesn't read manifest list yet, it only leverage
SnapshotRef
andSnapshot
to calculate expired snapshots. It's adding complexity and breaks the do one thing and do it well philosophy.
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 agree it's attempting. But I would prefer to use a maintenance API, for the following reasons: Currently ExpireSnapshots extends PendingUpdate<List>, if we are going to remove unused spec(and maybe unused schemas as well), we have to change the interface signature, which is breaking change.
ExpireSnapshots doesn't read manifest list yet, it only leverage SnapshotRef and Snapshot to calculate expired snapshots. It's adding complexity and breaks the do one thing and do it well philosophy.
-
I don't think this is necessarily true that we need to change the interface signature. We are still producing a new set of snapshots that are removed as part of the procedure but in addition to that we are (in an opt-in manner) pruning out the unused specs/schemas which is orthogonal to snapshots for this purpose.
-
As part of file cleanup the procedure does determine which files are still referenced (going through manifest lists/manifests) and which should be removed (this is in the FileCleanupStrategy implementations like
ReachableFileCleanup
. I think it's true that it adds a bit of complexity but the complexity is tracking which partition specs are referenced as we traverse the manifests. The same users which are frequently running expire snapshots also probably want to get rid of unused specs/schemas to keep metadata sizes smaller. After some more thought, I feel like it aligns with "one thing and do it well" since the "one thing" logic we're talking about is already common (the logic to traverse manifests) relative to expanding the API surface
}); | ||
} | ||
|
||
private TableMetadata removeUnusedSpecs(TableMetadata current) { |
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 other operations typically call this method internalApply
when it is the core functionality of apply
but the class needs to call it from both apply
and commit
.
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.
Let me rename it to internalApply
then.
MetadataTableUtils.createMetadataTableInstance(table, MetadataTableType.ALL_ENTRIES) | ||
.newScan() | ||
.planFiles(), | ||
task -> ((BaseEntriesTable.ManifestReadTask) task).partitionSpecId())); |
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.
We generally avoid unchecked casts because this creates a brittle dependency on a particular type being produced. That in turn limits our ability to trust the type system and make reasonable changes quickly.
If I understand correctly, the purpose of using a metadata table here is not to use the Table
interface, but instead to reuse some of the code in the all_entries
table. I think a more direct path would be to use ManifestGroup
and possibly refactor some of the logic from the all_entries
table to be more easily reused.
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.
Good point, let me refactor this to avoid unchecked cast.
If I understand correctly, the purpose of using a metadata table here is not to use the Table interface, but instead to reuse some of the code in the all_entries table
I think this code is used to avoid actually accessing the underlying rows, see #3462 (comment). We can/should use the Table
interface to access Manifest files directly.
How does that sound to you?
@@ -1102,6 +1121,22 @@ public Builder setDefaultPartitionSpec(int specId) { | |||
return this; | |||
} | |||
|
|||
private Builder removePartitionSpec(PartitionSpec spec) { | |||
Preconditions.checkArgument( | |||
changes.isEmpty(), "Cannot remove partition spec with other metadata update"); |
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 necessary?
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 this is needed to avoid conflicts with changes to the default spec ID. I'd probably change this to allow other changes and instead update this and the methods that set the default spec ID to check for one another.
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 this is needed to avoid conflicts with changes to the default spec ID.
Yeah, besides SetDefaultSepc
, AddPartitionSpec
might also interfere with this. For safety purposes, the previous implementation rejects all other metadata updates since removePartitionSpec
is rarely called and always called alone.
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 @rdblue and @amogh-jahagirdar for reviewing, will address them in a new commit.
core/src/main/java/org/apache/iceberg/BaseRemoveUnusedSpecs.java
Outdated
Show resolved
Hide resolved
}); | ||
} | ||
|
||
private TableMetadata removeUnusedSpecs(TableMetadata current) { |
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.
Let me rename it to internalApply
then.
core/src/main/java/org/apache/iceberg/BaseRemoveUnusedSpecs.java
Outdated
Show resolved
Hide resolved
MetadataTableUtils.createMetadataTableInstance(table, MetadataTableType.ALL_ENTRIES) | ||
.newScan() | ||
.planFiles(), | ||
task -> ((BaseEntriesTable.ManifestReadTask) task).partitionSpecId())); |
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.
Good point, let me refactor this to avoid unchecked cast.
If I understand correctly, the purpose of using a metadata table here is not to use the Table interface, but instead to reuse some of the code in the all_entries table
I think this code is used to avoid actually accessing the underlying rows, see #3462 (comment). We can/should use the Table
interface to access Manifest files directly.
How does that sound to you?
/** | ||
* Prune the unused partition specs from the table metadata. | ||
* | ||
* <p>Note: it's not safe for external client to call this directly, it's usually called by the |
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.
Let me remove the note then.
Also, we are no longer adding new operation methods to this. These days we make modifications to the Builder instead.
For this part, I think modifications are still going through the Builders. The main purpose of this method is to provide a single access point to prune unused specs purely so that prune unused specs are not mixed with other metadata updates.
@@ -1102,6 +1121,22 @@ public Builder setDefaultPartitionSpec(int specId) { | |||
return this; | |||
} | |||
|
|||
private Builder removePartitionSpec(PartitionSpec spec) { | |||
Preconditions.checkArgument( | |||
changes.isEmpty(), "Cannot remove partition spec with other metadata update"); |
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 this is needed to avoid conflicts with changes to the default spec ID.
Yeah, besides SetDefaultSepc
, AddPartitionSpec
might also interfere with this. For safety purposes, the previous implementation rejects all other metadata updates since removePartitionSpec
is rarely called and always called alone.
toBeRemoved != null && toBeRemoved.equals(spec), | ||
"Cannot remove an unknown spec, spec id: %s", | ||
spec.specId()); | ||
this.specs = |
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.
There's no need to fail something if it has an accidental retry after the first attempt succeeded.
This is a very good point, let me reconsider this part.
if (hasRemovedSpecs) { | ||
Preconditions.checkArgument( | ||
changes.isEmpty(), "Cannot remove partition specs with other metadata update"); |
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.
See https://github.com/apache/iceberg/pull/10755/files#r1690420193 and https://github.com/apache/iceberg/pull/10755/files#r1696099907
I think this check is to make sure that RemoveUnusedSpecs
and other metadata updates are not happened together.
@@ -1425,6 +1460,7 @@ private boolean hasChanges() { | |||
|| (discardChanges && !changes.isEmpty()) | |||
|| metadataLocation != null | |||
|| suppressHistoricalSnapshots | |||
|| hasRemovedSpecs |
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 way so that we avoid having to add the metadata update type for REST (since then that would ultimately just be in the changes list)?
Yes, as discussed earlier, I don't think we should expose removePartitionSpec
directly to the REST API as there's no easy way to ensure that the spec to remove is indeed not used.
This is not the right way to update hasChanges. Instead, this needs to add a change to changes.
It was adding a RemovePartitionSpec
to the changes. However, that would require a REST spec change and it's a bit heavy. See discussions as well: #10755 (comment)
That way it is sent to REST services to modify the table in the REST commit path.
I might be missing something, so all the metadata changes have to be sent to the REST service? I didn't work with a REST catalog before and don't see how changes are sent to the REST service. It would be great that some reference or code could be pointed to.
core/src/main/java/org/apache/iceberg/BaseRemoveUnusedSpecs.java
Outdated
Show resolved
Hide resolved
core/src/main/java/org/apache/iceberg/BaseRemoveUnusedSpecs.java
Outdated
Show resolved
Hide resolved
|
||
Table loaded = catalog.loadTable(TABLE); | ||
assertThat(loaded.specs().values()) | ||
.hasSameElementsAs(Lists.asList(spec, current, new PartitionSpec[0])); |
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.
containsExactly(..)
table.updateSpec().addField(Expressions.bucket("data", 16)).commit(); | ||
table.updateSpec().removeField(Expressions.bucket("data", 16)).commit(); | ||
table.updateSpec().addField("data").commit(); | ||
assertThat(table.specs().size()).as("Should have 3 total specs").isEqualTo(3); |
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.
assertThat(table.specs().size()).as("Should have 3 total specs").isEqualTo(3); | |
assertThat(table.specs()).as("Should have 3 total specs").hasSize(3); |
@nastra Thanks for reviewing, all your comments should be addressed. |
core/src/test/java/org/apache/iceberg/TestUpdateRequirements.java
Outdated
Show resolved
Hide resolved
core/src/test/java/org/apache/iceberg/TestUpdateRequirements.java
Outdated
Show resolved
Hide resolved
SnapshotRef snapshotRef = mock(SnapshotRef.class); | ||
when(snapshotRef.snapshotId()).thenReturn(snapshotId); | ||
when(snapshotRef.isBranch()).thenReturn(true); | ||
when(metadata.refs()).thenReturn(ImmutableMap.of(branch, snapshotRef)); |
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 also requires mocking this: when(metadata.ref(branch)).thenReturn(snapshotRef);
(it wasn't failing because the call to validate()
was missing)
core/src/test/java/org/apache/iceberg/catalog/CatalogTests.java
Outdated
Show resolved
Hide resolved
core/src/test/java/org/apache/iceberg/catalog/CatalogTests.java
Outdated
Show resolved
Hide resolved
* reachable by any snapshot | ||
* @return this for method chaining | ||
*/ | ||
default ExpireSnapshots removeUnusedSpecs(boolean removeUnusedSpecs) { |
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 @advancedxy ! I'm in favor of the client side API option for now, just had a comment on the name of it.
I think there's an important discussion to be had on how REST servers can indicate to clients the specific supported update types, this can either be done through config endpoint or through the existing endpoints discovery mechanism where each update type is attached as a query fragment to the endpoint and clients just use that.
My opinion is that it's not worth blocking this on a REST protocol discussion since we can always just come back and deprecate this client side option and have it run as part of the default procedure implementation.
@nastra @amogh-jahagirdar PTAL again, I think all your comments are addressed. |
.forEach( | ||
(name, ref) -> { | ||
if (ref.isBranch() && !name.equals(SnapshotRef.MAIN_BRANCH)) { | ||
require(new UpdateRequirement.AssertRefSnapshotID(name, ref.snapshotId())); |
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.
we should probably also have a test in TestUpdateRequirements
that actually changes the branch and eventually fails
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.
in addition to this, CatalogTests
currently only tests the happy path, but not the exceptions that are either thrown by AssertDefaultSpecID
or AssertRefSnapshotID
. We need to test both cases in CatalogTests
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 updated the TestUpdateRequirements
. However, it's hard to test the failure cases in CatalogTests: before committing, the internalApply
also refreshes the base table, so we cannot create a concurrent update to base table easily, which has to be after the refresh and before commit. Do you have any suggestions?
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 your patience and dilligent work @advancedxy, that's really appreciated! I think this is very close just had a comment on the builder API and the JavaDoc.
* Allows expiration of unreachable metadata, such as partition spec as part of the operation. | ||
* | ||
* @param clean setting this to true will remove metadata(such as partition spec) that is no | ||
* longer reachable by any snapshot | ||
* @return this for method chaining |
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.
/**
* Allows expiration of unreachable table layout metadata, such as partition specs as part of the operation.
*
* @param setting this to true will remove table layout metadata that is no
* longer reachable by any snapshot
* @return this for method chaining
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.
Basically don't think we need to mention the "such as partition spec" twice, I think once at the top level and clarifying that we're talking about layout metadata like the spec should be sufficient imo.
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.
Good point, fixed.
@@ -1108,6 +1108,25 @@ public Builder setDefaultPartitionSpec(int specId) { | |||
return this; | |||
} | |||
|
|||
Builder removeSpecIds(Iterable<Integer> specIds) { |
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 I'd prefer this be called removeSpecs
. If we look at the other APIs on TableMetadata builder like setDefaultPartitionSpec(int specId)
it aligns with that naming, I don't think we need the "Ids" suffix. In case we ever need a removeSpecs which takes in actual PartitionSpec we'd just overload at that time just like setDefaultPartitionSpec
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'm not super opinionated here though, I remember the early discussion we had where I also mentioned removeSpecIds but this is just something I noticed when taking a look just now.
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.
Updated too. I do think removeSpecs
is more consistent with other APIs in the TableMetadata builder.
@amogh-jahagirdar @nastra since all comments are addressed, do you think it's ready to merge? I'm happy to address more comments if needed. |
Thanks @advancedxy , I'll leave it open for a bit since @danielcweeks also mentioned he would take a look. But from my side everything LGTM. |
@danielcweeks would you mind to take a look at this? |
* operation. | ||
* | ||
* @param clean setting this to true will remove table layout metadata that is no longer reachable | ||
* by any snapshot |
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 a bit verbose. I'd recommend being more direct and not making it a full sentence. Something like:
remove unused partition specs, schemas, or other metadata when true
The same could apply to the method description:
Enable cleaning up unused partition specs, schemas, or other 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.
Good point, Updated.
@@ -273,7 +275,7 @@ private Set<String> findFilesToDelete( | |||
Set<ManifestFile> manifestsToScan, | |||
Set<ManifestFile> manifestsToRevert, | |||
Set<Long> validIds, | |||
TableMetadata current) { | |||
Map<Integer, PartitionSpec> specsById) { |
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.
+1 for fixing this. In general we should avoid passing huge metadata objects around to helper methods that don't need everything!
base.snapshot(snapshot).allManifests(ops.io()).stream() | ||
.map(ManifestFile::partitionSpecId) | ||
.forEach(reachableSpecs::add)); | ||
Set<Integer> specsToRemove = |
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: newline after a loop.
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.
addressed.
This is a continue work of #3462, all the credits should goes to @RussellSpitzer.
Previously there was no way to remove partition specs from a table once they were
added. To fix this we add an api which searches through all reachable manifest
files and records their specsIds. Any specIds which do not find are marked for
removal which is done through a serializable commit.