Repository navigation
feat(rest): support table commit leases - #474
Merged
Merged
Conversation
Port REST catalog table commit leases from Java Paimon. The table property rest.commit.lock-enabled turns on server-side lease enforcement, and a writer that sets rest.commit.acquire-lock acquires a lease for each publication attempt. Both default to false. Asking to acquire without the table capability, without a table UUID, or in a request-only commit built with UseRESTCatalogCommit() fails when the commit is created. Add an internal CommitLock/CommitLockScope interface mirroring Java Lock. SnapshotCommit exposes AcquireLock(), CatalogSnapshotCommit holds the lock created by VersionManagedCatalog::CreateCommitLock(), and RestCatalog returns a RestCommitLock bound to the branch identifier, table UUID and commit user. RestCommitLock acquires a lease through the commit-lock endpoint. While another writer holds it, acquisition logs that it is waiting, then polls until commit.timeout without using up commit.max-retries; the timeout is checked after each response rather than interrupting a request in flight. A grant for another commit user or without a positive lease duration is rejected with an error naming the expected and actual value, and a renewal thread that cannot be started fails the acquisition instead of throwing. Each scope renews its lease through commit-lock/renew every third of the lease duration and tracks the deadline on a monotonic clock from when each grant was requested, so a refused or failed renewal, a changed lease duration or a local expiry invalidates it. There is no unlock request: ending a scope only stops renewing, and never waits for a renewal in flight, even one refreshing credentials, since renewal runs on a detached thread sharing the lease state. The renewal request is cancelled through a new RestHttpClient::Cancellation, which aborts the libcurl transfer and ends a retry backoff. FileStoreCommitImpl acquires a scope before reading the head of each attempt, checks it in CommitSnapshotImpl right before SnapshotCommit::Commit(), and releases it before waiting to retry. A failed acquisition, or a scope found invalid, publishes nothing and is retried within the commit.max-retries and commit.timeout budget of a conflict. An attempt that lost its scope removes the manifests it wrote, as one that lost a race does, and that cleanup now also covers changelog manifests and their list. Running out of retries reports the lease failure instead of a conflict. RollbackToAsLatest holds a scope too; failing to acquire or losing it fails the rollback, and a lost scope also removes the manifests it wrote.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Purpose
Linked issue: close #470
Java Paimon added table commit leases to the REST catalog (apache/paimon#10491, ownership
clarified in apache/paimon#10588). paimon-cpp knew neither option, so a writer configured with
rest.commit.acquire-lock=truecommitted without a lease and without an error, raced writersthat hold one, and was rejected by the server while a Java writer held the lease.
Port the client side of REST commit leases for commits through
WithCatalog(), aligned withJava
RESTCatalogLockandRESTCatalogLockFactory.rest.commit.lock-enabled(table property) andrest.commit.acquire-lock(writeroption), both default
false. Asking to acquire without the table capability fails with theJava message before any REST request; the table property alone does not make a writer acquire.
A commit lease also requires the table UUID passed to
WithTableId(), and a request-onlycommit built with
UseRESTCatalogCommit()rejectsrest.commit.acquire-lock=true.commit-lockandcommit-lock/renewendpoints withCommitLockRequestandCommitLockResponse.CommitLock/CommitLockScopeinterface mirroring JavaLock.SnapshotCommit::AcquireLock()returns the scope of one publication attempt,CatalogSnapshotCommitholds the lock created byVersionManagedCatalog::CreateCommitLock(),and
RestCatalogreturns aRestCommitLockbound to the branch identifier, table UUID andcommit user.
RestCommitLockpolls everymin(100ms, commit.timeout)while another writer holds thelease, without using up
commit.max-retries, and rejects a grant for another commit user orwithout a positive
leaseMillis. Each scope renews its lease everyleaseMillis / 3on adetached thread and tracks the deadline on a monotonic clock from when each grant was
requested, so a refused or failed renewal, a changed
leaseMillisor a local expiryinvalidates it. Ending a scope only stops renewing; there is no unlock request. A new
RestHttpClient::Cancellationaborts the renewal transfer and its retry backoff.FileStoreCommitImplacquires a scope before reading the head of each attempt, checks itright before
SnapshotCommit::Commit()and releases it before waiting to retry. A failedacquisition or a lost scope publishes nothing and is retried within the
commit.max-retriesand
commit.timeoutbudget of a conflict; an attempt that lost its scope removes themanifests it wrote, now including changelog manifests and their list.
RollbackToAsLatestholds a scope too and fails when it cannot acquire or loses it.
File-system commits are unchanged.
Tests
RestCommitLockTestinsrc/paimon/rest/rest_commit_lock_test.cpp, ported from JavaRESTCatalogLockTest:MissingIdentityIsRejectedBeforeAnyRequest: no table UUID, commit user or valid timeout.AcquirePollsUntilGrantedandContentionTimesOutWithoutRenewing: contention polls untilgranted, or fails once the timeout elapses.
FailedAcquisitionStartsNoLease: 400, 403 and 404 map to invalid argument, no permissionand table not found; a grant for another commit user or with
leaseMillis=0and a renewalthread that cannot start fail the acquisition without renewing.
RenewalExtendsTheLeaseFromEachRequest,FailedRenewalInvalidatesTheLeaseandLocalExpiryInvalidatesTheLease: the deadline is measured from each request; a renewalanswered
acquired=false, an error, another owner or a changedleaseMillis, and a localexpiry, invalidate the lease.
EndingTheScopeDoesNotWaitForARenewalInFlight,LateAnswerToAnEndedScopeCannotAffectTheNextScopeandLeasesOfDifferentWritersDoNotInterfere.src/paimon/rest/rest_catalog_test.cpp, following JavaRESTCommitLockIntegrationTestandCommitLockProtocolTest:RestCatalogTest.CommitLockAddressesTheTableOrBranch: leases address the table or branch,and system tables are rejected.
RestCatalogCommitLeaseTest.LeaseIsAcquiredBeforeTheHeadAndAgainAfterAConflict,LeaseNeedsBothOptionsAndTheTableUuid,ContendedLeaseIsAwaitedUntilCommitTimeout,LeaseExpiredLocallyBeforePublicationSendsNoCommitandRenewalRefusedBeforePublicationSendsNoCommit.FileStoreCommitImplTestinsrc/paimon/core/operation/file_store_commit_impl_test.cpp:TestCatalogCommitHoldsALockScopePerAttempt,TestCatalogCommitRetriesOnceTheLockScopeIsLost,TestCatalogCommitRetriesAFailedLockAcquisition,TestCommitWithoutAUsableLockIsNotCreatedand
TestRollbackToAsLatestHoldsALockScope;TestCatalogCommitToBranchTakesTheBranchSnapshotchecks the lock is bound to the branch identifier.
RestHttpClientTest.CancellationStopsWaitingForAResponse,CancellationEndsARetryBackoffand
CancelledRequestIsNotSentinsrc/paimon/rest/rest_http_client_test.cpp.CatalogSnapshotCommitTest.TestAcquireLockUsesTheLockItWasGiven.RestMessagesTest.CommitLockRequestRoundTripandCommitLockResponseRoundTrip,ResourcePathsTest.WithPrefix, andCoreOptionsTest.TestDefaultValueandTestFromMap.API and Format
include/paimon/defs.haddsOptions::REST_COMMIT_LOCK_ENABLEDandOptions::REST_COMMIT_ACQUIRE_LOCK. No storage format changes.On the wire, a writer with
rest.commit.acquire-lock=truesendsPOST .../commit-lockandPOST .../commit-lock/renewwithtableIdandcommitUser, as the Java client does. Otherrequests are unchanged.
Documentation
Yes.
docs/source/user_guide/catalog.rstdescribes both options, how a lease is acquired,renewed and checked per attempt, how
commit.timeoutbounds the wait, and that a request-onlycommit cannot hold a lease.
Generative AI tooling
Generated-by: Claude Code 2.1.311 (Claude Opus 5.5)
🤖 Generated with Claude Code