Skip to content

[#12451] improvement(core): add OCC for metalake writes - #12454

Open
yuqi1129 wants to merge 2 commits into
apache:mainfrom
yuqi1129:feat/12342-occ-metalake
Open

[#12451] improvement(core): add OCC for metalake writes#12454
yuqi1129 wants to merge 2 commits into
apache:mainfrom
yuqi1129:feat/12342-occ-metalake

Conversation

@yuqi1129

@yuqi1129 yuqi1129 commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

Add database-backed optimistic concurrency control and transaction boundaries for metalake writes.

  • Advance the metalake OCC version on every alter, and guard alter and delete with a compare-and-set on the observed version.
  • Classify a failed CAS as either a stale-version conflict or an already-missing entity.
  • Keep the root CAS and the non-empty check or the cascade cleanup in one database transaction. Change-log emission now happens at the JDBCBackend mutation boundary after [#12151] improvement(core): complete cached entity change-log coverage #12374, so this PR no longer touches it.
  • Lock the catalog rows before a metalake cascade snapshot, then CAS-delete descendant catalogs and schemas with their observed identifier-and-version pairs, so a concurrent child write is reported instead of silently dropped.
  • Add the shared OptimisticLockException factories used by the follow-up catalog and schema PRs.

Rebased on current main (on top of #12374). This is the first of three PRs that replace #12350, which reviewers found too large. The stack is metalake -> catalog -> schema; each PR is independently green. Two cross-entity tests in TestMetalakeMetaService (concurrent schema alter during a metalake cascade, and metalake cascade racing a schema create) land with the schema PR, because the behaviour they assert only exists once schema writes take the catalog row lock and bump the schema version.

Why are the changes needed?

Managed metalake operations previously consisted of multiple independent reads and writes. Concurrent alter and delete requests could overwrite newer metadata, and a cascade delete could run partial cleanup while another writer was still modifying descendants.

Fix: #12451

Does this PR introduce any user-facing change?

Concurrent metalake version conflicts are reported as HTTP 409. If the observed entity was deleted or renamed away, alter reports not found and drop preserves its idempotent false result.

How was this patch tested?

  • ./gradlew :core:test :core:javadoc :catalogs:catalog-fileset:test :catalogs:catalog-kafka:test -PskipITs (H2)
  • New tests in TestMetalakeMetaService, TestMetalakeManager, TestExceptionUtils, TestPOConverters.
  • MySQL and PostgreSQL coverage for the new <foreach> CAS delete and the PostgreSQL lock syntax is left to CI (-PskipDockerTests=false).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Introduces database-backed optimistic concurrency control (OCC) for metalake writes in the relational store, ensuring version-guarded alters/deletes and transactional boundaries for cascade operations so concurrent writers are surfaced as conflicts instead of silently overwriting metadata.

Changes:

  • Advance metalake OCC version on every update and guard metalake update/delete with version compare-and-set (CAS) semantics.
  • Add shared ExceptionUtils factories for consistent OptimisticLockException messages and update metalake manager to preserve idempotent drop behavior when a concurrent delete wins.
  • Add CAS-based cascade deletion helpers that lock catalog rows and soft-delete catalogs/schemas using identifier-and-version pairs, plus new unit tests.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
core/src/main/java/org/apache/gravitino/metalake/MetalakeManager.java Treat concurrent underlying store delete (NoSuchEntityException) as an idempotent false drop result.
core/src/main/java/org/apache/gravitino/storage/relational/mapper/CatalogMetaMapper.java Add mapper APIs for locking catalog rows and CAS soft-deleting catalogs by (id, version).
core/src/main/java/org/apache/gravitino/storage/relational/mapper/CatalogMetaSQLProviderFactory.java Expose SQL provider hooks for catalog row locking and CAS delete-by-version.
core/src/main/java/org/apache/gravitino/storage/relational/mapper/MetalakeMetaMapper.java Add selectMetalakeMetaByIdForUpdate and versioned soft-delete signature for metalakes.
core/src/main/java/org/apache/gravitino/storage/relational/mapper/MetalakeMetaSQLProviderFactory.java Wire provider factory methods for FOR UPDATE metalake select and versioned soft delete.
core/src/main/java/org/apache/gravitino/storage/relational/mapper/SchemaMetaMapper.java Add list-by-metalake API and CAS soft-delete-by-version for schemas; remove metalake-wide schema soft-delete.
core/src/main/java/org/apache/gravitino/storage/relational/mapper/SchemaMetaSQLProviderFactory.java Add provider factory methods for list-by-metalake and CAS soft-delete-by-version.
core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/CatalogMetaBaseSQLProvider.java Add FOR UPDATE catalog listing and CAS delete-by-version SQL generation.
core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/MetalakeMetaBaseSQLProvider.java Add FOR UPDATE metalake select; update soft-delete to require matching current_version.
core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/SchemaMetaBaseSQLProvider.java Add list schemas by metalake and CAS delete-by-version SQL generation; adjust catalog-wide soft delete.
core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/CatalogMetaPostgreSQLProvider.java Implement PostgreSQL-specific CAS delete-by-version for catalogs.
core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/MetalakeMetaPostgreSQLProvider.java Update metalake soft-delete to be version-guarded; update update statement to version-based CAS.
core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/SchemaMetaPostgreSQLProvider.java Implement PostgreSQL-specific CAS delete-by-version for schemas; adjust catalog-wide soft delete SQL.
core/src/main/java/org/apache/gravitino/storage/relational/service/MetalakeMetaService.java Enforce metalake update/delete CAS behavior, transactional fencing, and CAS-based cascade deletion with locking.
core/src/main/java/org/apache/gravitino/storage/relational/utils/ExceptionUtils.java Add shared OptimisticLockException factory helpers for concurrent modification scenarios.
core/src/main/java/org/apache/gravitino/storage/relational/utils/POConverters.java Make metalake updates always advance OCC token (current/last versions aligned).
core/src/test/java/org/apache/gravitino/metalake/TestMetalakeManager.java Add test verifying drop returns false when a concurrent delete wins (store throws NoSuchEntityException).
core/src/test/java/org/apache/gravitino/storage/relational/service/TestMetalakeMetaService.java Add tests for version-based CAS behavior and conflict/missing classification paths.
core/src/test/java/org/apache/gravitino/storage/relational/utils/TestExceptionUtils.java Add unit tests for the new shared optimistic-lock exception messages.
core/src/test/java/org/apache/gravitino/storage/relational/utils/TestPOConverters.java Update converter tests to assert metalake version increments on update.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@yuqi1129
yuqi1129 force-pushed the feat/12342-occ-metalake branch from e7224b4 to b13af59 Compare August 13, 2026 07:41
Advance the metalake OCC version on every alter and guard alter and
delete with a compare-and-set on the observed version, classifying a
failed CAS as either a stale conflict or a missing entity.

Keep the metalake root CAS and the non-empty check or the cascade
cleanup inside one database transaction. A cascade locks the catalog
rows first, then compare-and-set deletes descendant catalogs and schemas
with their observed identifier-and-version pairs, so a concurrent child
write is reported instead of silently dropped.

Also add the shared OptimisticLockException factories used by the
follow-up catalog and schema changes.
@yuqi1129
yuqi1129 force-pushed the feat/12342-occ-metalake branch from b13af59 to 29db0ce Compare August 13, 2026 08:07
@yuqi1129

Copy link
Copy Markdown
Contributor Author

Thanks for the review. Responses to the three Copilot comments:

1. metalakeWriteFailure() uses a locking read — keeping the locking read, but I have documented why in the code.

The lock is load-bearing for correctness, not incidental. Under MySQL REPEATABLE READ a plain SELECT returns this transaction's snapshot, which still shows a row that a concurrent writer has already deleted or renamed away. That makes "stale version" and "already gone" indistinguishable, and a drop that lost the race would report a 409 conflict instead of staying idempotent. A locking read observes the latest committed row.

It also does not add waiting in practice: the compare-and-set is an UPDATE, so it has already queued on that same row lock. By the time this classification path runs, the competing writer has committed.

2 & 3. Row-value IN instead of the OR-chain in the CAS delete — not taking this for now.

  • The pattern applies equally to the MySQL/H2 base provider, so adopting it only in the PostgreSQL provider would fragment the dialects for an unmeasured gain.
  • It does not address the real scaling limit. Both forms bind two parameters per row, so a sufficiently wide cascade hits PostgreSQL's 65535 bind-parameter ceiling either way. If cascade width becomes a concern, the fix is to chunk the delete, which belongs in a separate change with a benchmark behind it.

Pushed the clarifying comment for (1); the same note is applied to the equivalent paths in #12455 and #12456.

@yuqi1129

Copy link
Copy Markdown
Contributor Author

@jerryshao
Advice accepted, and this is the first one for #12350.

@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown

Code Coverage Report

Overall Project 68.69% +0.29% 🟢
Files changed 85.61% 🟢

Module Coverage
aliyun 1.72% 🔴
api 49.31% 🟢
authorization-common 85.96% 🟢
aws 42.04% 🟢
azure 2.47% 🔴
catalog-common 9.92% 🔴
catalog-fileset 79.77% 🟢
catalog-glue 68.95% 🟢
catalog-hive 79.4% 🟢
catalog-jdbc-common 45.7% 🟢
catalog-jdbc-doris 81.8% 🟢
catalog-jdbc-mysql 79.33% 🟢
catalog-jdbc-postgresql 83.39% 🟢
catalog-jdbc-starrocks 79.16% 🟢
catalog-kafka 77.01% 🟢
catalog-lakehouse-generic 59.18% 🟢
catalog-lakehouse-hudi 79.1% 🟢
catalog-lakehouse-iceberg 85.86% 🟢
catalog-lakehouse-paimon 84.23% 🟢
catalog-model 77.72% 🟢
cli 44.48% 🟢
client-java 78.56% 🟢
common 52.75% 🟢
core 83.67% +0.11% 🟢
filesystem-hadoop3 77.28% 🟢
flink 0.0% 🔴
flink-common 48.68% 🟢
flink-runtime 0.0% 🔴
gcp 14.12% 🔴
hadoop-auth 68.0% 🟢
hadoop-common 12.7% 🔴
hive-metastore-common 53.4% 🟢
iceberg-aliyun-bundle 0.0% 🔴
iceberg-common 64.75% 🟢
iceberg-rest-server 75.03% 🟢
idp-basic 86.02% 🟢
integration-test-common 0.0% 🔴
jobs 62.92% 🟢
lance-common 31.75% 🔴
lance-rest-server 63.47% 🟢
lineage 53.02% 🟢
optimizer 83.17% 🟢
optimizer-api 21.95% 🔴
server 87.38% 🟢
server-common 79.41% 🟢
spark 28.57% 🔴
spark-common 45.89% 🟢
tencent 69.84% 🟢
trino-connector 40.29% 🟢
Files
Module File Coverage
core MetalakeMetaSQLProviderFactory.java 100.0% 🟢
MetalakeMetaBaseSQLProvider.java 100.0% 🟢
MetalakeMetaService.java 98.65% 🟢
ExceptionUtils.java 88.89% 🟢
POConverters.java 88.33% 🟢
CatalogMetaSQLProviderFactory.java 85.71% 🟢
CatalogMetaBaseSQLProvider.java 83.33% 🟢
CatalogMetaPostgreSQLProvider.java 83.33% 🟢
MetalakeMetaPostgreSQLProvider.java 80.0% 🟢
SchemaMetaPostgreSQLProvider.java 75.0% 🟢
SchemaMetaSQLProviderFactory.java 72.73% 🟢
MetalakeManager.java 65.88% 🟢
SchemaMetaBaseSQLProvider.java 65.22% 🟢
CatalogMetaMapper.java 0.0% 🔴
MetalakeMetaMapper.java 0.0% 🔴
SchemaMetaMapper.java 0.0% 🔴

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated no new comments.

Suppressed comments (2)

core/src/main/java/org/apache/gravitino/storage/relational/service/MetalakeMetaService.java:449

  • Passing every schema in the metalake to one generated UPDATE uses two bind parameters per row. PostgreSQL and MySQL reject prepared statements beyond 65,535 parameters, so 32,768 schemas make cascade deletion fail, with the large OR tree potentially failing sooner. Process this snapshot in bounded chunks in the existing transaction and compare the total affected rows with the snapshot size.
    int deleted =
        SessionUtils.getWithoutCommit(
            SchemaMetaMapper.class, mapper -> mapper.softDeleteSchemaMetasWithVersion(schemaPOs));

core/src/main/java/org/apache/gravitino/storage/relational/service/MetalakeMetaService.java:430

  • This sends the entire catalog snapshot as one generated UPDATE, with two bind parameters per catalog. PostgreSQL and MySQL cap prepared statements at 65,535 parameters, so a metalake with 32,768 catalogs cannot be cascade-deleted; the long OR expression may hit parser or packet limits even earlier. Execute the CAS deletes in bounded chunks within this same transaction and sum the affected-row counts before committing.

This issue also appears on line 447 of the same file.

    int deleted =
        SessionUtils.getWithoutCommit(
            CatalogMetaMapper.class,
            mapper -> mapper.softDeleteCatalogMetasWithVersion(catalogPOs));

Comment on lines +324 to +333
() -> {
deleteMetalakeWithVersion(ident, metalakeId, currentVersion);
List<CatalogPO> catalogPOs =
SessionUtils.getWithoutCommit(
CatalogMetaMapper.class,
mapper -> mapper.listCatalogPOsByMetalakeId(metalakeId));
if (!catalogPOs.isEmpty()) {
throw new NonEmptyEntityException(
"Entity %s has sub-entities, you should remove sub-entities first", ident);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why do we delete the metalake first, then check if the catalog is empty or not?

Entity.EntityType.SCHEMA, Entity.EntityType.METALAKE, metalakeIdentifier);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't what's the performance now after changing to OCC. My feeling is that we add more db operations compared to before.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Subtask] Add version-CAS OCC for metalake writes

3 participants