Fix multi-schema replication intermediates dropped on snapshots-endpoint commits#603
Conversation
| boolean isReplicaCommit = | ||
| getTableType(base, metadata) == CreateUpdateTableRequestBody.TableTypeEnum.REPLICA_TABLE; | ||
| if (metadataUpdated && snapshotsUpdated && base != null && !isReplicaCommit) { |
There was a problem hiding this comment.
What makes a replica commit different that we need to special case?
There was a problem hiding this comment.
Does that mean that this excludes RTAS commit for replica table?
There was a problem hiding this comment.
This block is supposed to be used for RTAS/CTAS where these is no concept of intermediate schemas but the issue is that this also sources replica commits from gaas into replace snapshots loosing intermediate schemas.
| boolean isReplicaCommit = | ||
| getTableType(base, metadata) == CreateUpdateTableRequestBody.TableTypeEnum.REPLICA_TABLE; | ||
| if (metadataUpdated && snapshotsUpdated && base != null && !isReplicaCommit) { | ||
| // Only CTAS and RTAS can update both metadata and snapshots at the same time. |
There was a problem hiding this comment.
this comment is no longer accurate.
| .get(OPENHOUSE_CLUSTER_ID_KEY) | ||
| .equals(metadata.properties().get(OPENHOUSE_CLUSTER_ID_KEY))) { | ||
| return baseTableType; | ||
| String baseTypeStr = base.properties().get(OPENHOUSE_TABLE_TYPE_KEY); |
There was a problem hiding this comment.
Why might this not exist?
There was a problem hiding this comment.
This is added as a safety check for legacy tables.
There was a problem hiding this comment.
what are legacy tables?
| @VisibleForTesting | ||
| CreateUpdateTableRequestBody.TableTypeEnum getTableType( | ||
| TableMetadata base, TableMetadata metadata) { | ||
| String metaTypeStr = metadata.properties().get(OPENHOUSE_TABLE_TYPE_KEY); |
There was a problem hiding this comment.
nit, I would move this to after the if so on the case where base is null its not evaluated.
There was a problem hiding this comment.
This is needed for the end return statement.
| if (baseTableType == CreateUpdateTableRequestBody.TableTypeEnum.REPLICA_TABLE | ||
| && metadataTableType == CreateUpdateTableRequestBody.TableTypeEnum.PRIMARY_TABLE | ||
| && baseCluster != null | ||
| && !baseCluster.equals(metaCluster)) { | ||
| return baseTableType; | ||
| } |
There was a problem hiding this comment.
This logic does not makse sense to me. Why can we not trust the table type?
There was a problem hiding this comment.
Trying to understand the change here. Seems like this change is applicable only for replica table to determine table type?
There was a problem hiding this comment.
Just to be clear, this logic is pre-existing and this change only introduce null checks.
Metadata's type can be trusted by default; this branch is the one exception. In a cross-cluster replication commit, the destination replica's metadata is overwritten with the source primary's content, so metadata.tableType says PRIMARY even though this table is a REPLICA. base is the destination's truth, so for that fingerprint (base=REPLICA, metadata=PRIMARY, different cluster) we override to base.
I do agree that it can be collapsed to "base if non-null, else metadata". I would put it out of scope of this PR.
There was a problem hiding this comment.
To me the concept of a Replica table as a first class citizen is adding the friction here. A replica there is nothign special about other than the permissions on who can modify it. Its unclear to me why this is a table type, and why that needs a special case.
…int commits Replication commits arriving through PUT /iceberg/v2/snapshots with multi-schema-jump intermediate schemas were silently losing them due to two issues that combine to leave the dest replica with a snapshot whose schema-id points at a schema not present in schemas[]. Trino strictly resolves snapshot schema-id against schemas[] and fails with "Cannot find schema with schema id N"; Spark falls back to current-schema-id and reads succeed. Two fixes: 1. TablesMapper#toTableDto(TableDto, IcebergSnapshotsRequestBody) was missing the @mapping for newIntermediateSchemas. The CreateUpdateTableRequestBody overload had it (added in linkedin#407), but the IcebergSnapshotsRequestBody overload used for the snapshots endpoint did not — so intermediates packed correctly by the client got dropped at the server-side DTO mapping step. 2. OpenHouseTableOperations#doCommit (after linkedin#524 introduced putSnapshotsForReplace) was routing cross-cluster REPLICA_TABLE replication commits through the RTAS replace path because they update both metadata and snapshots in one commit. The server's REPLACE branch treats the commit as a fresh table creation and uses CLIENT_TABLE_SCHEMA (bootstrap-era schema) as the final schema, not the request's actual target. This produces a different but related corruption pattern. Detect REPLICA commits (base REPLICA_TABLE + metadata PRIMARY_TABLE + different cluster IDs) and route them through the regular putSnapshots path so the intermediate-schemas plumbing fires. Both fixes are needed in tandem: without (1), even the regular update branch drops intermediates; without (2), commits hitting the post-linkedin#524 client get routed around (1) entirely. Unit tests: - TablesMapperTest#testToTableDtoWithPutSnapshotsPreservesNewIntermediateSchemas pins the mapper extraction. - OpenHouseTableOperationsTest#testDoCommitRoutesReplicaTableThroughPutSnapshotsNotReplace pins REPLICA dispatch goes to regular putSnapshots (replaceCommit=false). - OpenHouseTableOperationsTest#testDoCommitRoutesPrimaryRtasThroughPutSnapshotsForReplace pins genuine RTAS on PRIMARY tables still uses the replace path.
Documentation-only follow-up to the multi-schema replication fix. No logic change. - Rewrite the putSnapshotsForReplace branch comment: the old "only CTAS and RTAS update both metadata and snapshots" was inaccurate now that REPLICA commits also touch both and are excluded via isReplicaCommit. - Ground getTableType's null return in the real cause (legacy tables predating openhouse.tableType, back-filled server-side by OpenHouseInternalRepositoryImpl#checkIfTableTypeAdded) instead of the vague "replace transactions before properties populate". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6db910e to
29a4ecd
Compare
abhisheknath2011
left a comment
There was a problem hiding this comment.
Looks good to me. Lets get a stamp from Mike as well.
| if (metadataUpdated && snapshotsUpdated && base != null) { | ||
| // Only CTAS and RTAS can update both metadata and snapshots at the same time. | ||
| // When the table exists, it will be a replace commit operation. | ||
| // REPLICA_TABLE commits from cross-cluster replication can update both |
There was a problem hiding this comment.
I don't think detecting that this is a replica seems incorrect vs detecting the type of change and applying it correctly. Why can't we detect the condition and apply it vs detecting the table type?
There was a problem hiding this comment.
The existing implementation already tries to detect a change-type: metadataUpdated && snapshotsUpdated && base != null is meant to mean "this is a replace (RTAS/CTAS)." The problem is a cross-cluster replication commit also updates both metadata and snapshots in one shot, so it matches that same condition — the two operations are indistinguishable by their metadata/snapshot deltas alone.
There was a problem hiding this comment.
That's what's unclear to me is why this needs a special commit type at all. This is what's unclear by this logic:
1 - Why does Replication have special handling at-all in this case? What is the distinguishing feature of Replication that requires it to be handled specifically
2 - Why is replication table type not enough of a signal to determine what to do?
I understand we need to preserve history but "replica" type seems like the wrong thing to be detecting, and if it is the thing that we are detecting then it should be enough to have its own dedicated logic "when replica do x"
mkuchenbecker
left a comment
There was a problem hiding this comment.
If we are keeping replica tables as separately handled, a clear "if replica, do X" will make the logic easier to follow and later remove if the case is no longer needed, and protects replication from changes to other commit types.
|
+1 to merge. this stops the data loss. But, this fix adds a fourth way to detect an intent the client already knows but never states. Server infers "replace vs. not" from comparing before/after states and then corrects that guess with three table properties (is-a-copy, is-an-original, cluster names differ) and now a further check to steer replication back to the update path. Each is a workaround for the same root which is that the caller knows what operation it's doing and doesn't send that operation to the server, so the server reconstructs it. Every new commit shape will need another clue and another branch. The durable fix is for the client to declare the operation instead of the server deducing it. This is what the open-source Iceberg REST catalog does that we can draw inspiration from. A commit is an explicit list of changes to apply (add this schema, set current schema, add this snapshot, point this branch at it) plus preconditions, with no "replace mode" to detect. Replication stops being special, same change list as anything else, just more "add schema" entries. With that in mind, I suggest a not-blocking follow-up: add one field where the caller declares the operation (replication / replace / append) and route the logic on that enum in server. then delete the metadata-comparison guess and the property-sniffing that was bandaided over time. Longer term, we can align with the REST spec [0] and replace the two schema fields with one ordered list of schema additions. Today the final schema goes in one field and the in-between schema versions go in a separate list and it is handled as a branched logic on server. Reducing branching logic helps prevent bugs on edge cases. And then openhouse.tableType and openhouse.clusterId can stop being used to pick a code path, as sort of side-affect magic variables. [0] - In the Iceberg REST catalog, a commit (
|
| putSnapshots(base, metadata); | ||
| } else if (metadataUpdated) { | ||
| createUpdateTable(base, metadata); | ||
| } |
There was a problem hiding this comment.
nit. else not needed, just return short circuit.
Summary
Replication commits arriving through PUT /iceberg/v2/snapshots with multi-schema-jump intermediate schemas were silently losing them due to two issues that combine to leave the dest replica with a snapshot whose schema-id points at a schema not present in schemas[]. Trino strictly resolves snapshot schema-id against schemas[] and fails with "Cannot find schema with schema id N"; Spark falls back to current-schema-id and reads succeed.
Two fixes:
TablesMapper#toTableDto(TableDto, IcebergSnapshotsRequestBody) was missing the @mapping for newIntermediateSchemas. The CreateUpdateTableRequestBody overload had it (added in Add multi schema update support for tables during replica table commits #407), but the IcebergSnapshotsRequestBody overload used for the snapshots endpoint did not — so intermediates packed correctly by the client got dropped at the server-side DTO mapping step.
OpenHouseTableOperations#doCommit (after [RTAS] Add client side support (part 3) #524 introduced putSnapshotsForReplace) was routing cross-cluster REPLICA_TABLE replication commits through the RTAS replace path because they update both metadata and snapshots in one commit. The server's REPLACE branch treats the commit as a fresh table creation and uses CLIENT_TABLE_SCHEMA (bootstrap-era schema) as the final schema, not the request's actual target. This produces a different but related corruption pattern. Detect REPLICA commits (base REPLICA_TABLE + metadata PRIMARY_TABLE + different cluster IDs) and route them through the regular putSnapshots path so the intermediate-schemas plumbing fires.
Both fixes are needed in tandem: without (1), even the regular update branch drops intermediates; without (2), commits hitting the post-#524 client get routed around (1) entirely.
Changes
For all the boxes checked, please include additional details of the changes made in this pull request.
Testing Done
For all the boxes checked, include a detailed description of the testing done for the changes made in this pull request.
Additional Information
For all the boxes checked, include additional details of the changes made in this pull request.