This is an automated email from the ASF dual-hosted git repository.
yuqi1129 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new 424cc10401 [#12341] improvement(server): define optimistic-lock
conflict response (#12349)
424cc10401 is described below
commit 424cc10401c1aadfc12974e304ab569e78a8d976
Author: Qi Yu <[email protected]>
AuthorDate: Tue Aug 4 16:05:23 2026 +0800
[#12341] improvement(server): define optimistic-lock conflict response
(#12349)
### What changes were proposed in this pull request?
- Add `OptimisticLockException` as the shared optimistic-concurrency
conflict contract.
- Add error code 1012 and an optimistic-lock error response.
- Map optimistic-lock conflicts to HTTP 409.
- Add response, server utility, and REST exception-handler tests.
This PR only defines the shared contract. Entity-specific CAS behavior
will be implemented by the follow-up subtasks of #12166.
### Why are the changes needed?
Entity write paths need a typed conflict signal instead of relying on
generic exceptions or matching error-message prefixes. A shared
exception and REST mapping allow later entity PRs to report lost CAS
operations consistently.
Fix: #12341
### Does this PR introduce _any_ user-facing change?
Yes. It introduces the public `OptimisticLockException` type and error
code 1012. REST operations that throw this exception are returned as
HTTP 409 Conflict.
### How was this patch tested?
- `./gradlew spotlessApply`
- `./gradlew :common:test --tests
'org.apache.gravitino.dto.responses.TestResponses' :server-common:test
--tests 'org.apache.gravitino.server.web.TestUtils' :server:test --tests
'org.apache.gravitino.server.web.rest.TestExceptionHandlers' -PskipITs
-PskipDockerTests=true`
---
.../exceptions/OptimisticLockException.java | 53 ++++++++++++++++++++++
.../gravitino/dto/responses/ErrorConstants.java | 3 ++
.../gravitino/dto/responses/ErrorResponse.java | 14 ++++++
.../gravitino/dto/responses/TestResponses.java | 10 ++++
.../org/apache/gravitino/server/web/Utils.java | 16 +++++++
.../org/apache/gravitino/server/web/TestUtils.java | 16 +++++++
.../server/web/rest/ExceptionHandlers.java | 6 +++
.../server/web/rest/TestExceptionHandlers.java | 19 ++++++++
8 files changed, 137 insertions(+)
diff --git
a/api/src/main/java/org/apache/gravitino/exceptions/OptimisticLockException.java
b/api/src/main/java/org/apache/gravitino/exceptions/OptimisticLockException.java
new file mode 100644
index 0000000000..34451063e6
--- /dev/null
+++
b/api/src/main/java/org/apache/gravitino/exceptions/OptimisticLockException.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.exceptions;
+
+import com.google.errorprone.annotations.FormatMethod;
+import com.google.errorprone.annotations.FormatString;
+
+/**
+ * Exception thrown when an optimistic-lock update fails because the entity
changed after it was
+ * read.
+ */
+public class OptimisticLockException extends GravitinoRuntimeException {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Constructs a new exception with the specified detail message.
+ *
+ * @param message the detail message.
+ * @param args the arguments to the message.
+ */
+ @FormatMethod
+ public OptimisticLockException(@FormatString String message, Object... args)
{
+ super(message, args);
+ }
+
+ /**
+ * Constructs a new exception with the specified detail message and cause.
+ *
+ * @param cause the cause.
+ * @param message the detail message.
+ * @param args the arguments to the message.
+ */
+ @FormatMethod
+ public OptimisticLockException(Throwable cause, @FormatString String
message, Object... args) {
+ super(cause, message, args);
+ }
+}
diff --git
a/common/src/main/java/org/apache/gravitino/dto/responses/ErrorConstants.java
b/common/src/main/java/org/apache/gravitino/dto/responses/ErrorConstants.java
index 870addfba9..8b94b0df3e 100644
---
a/common/src/main/java/org/apache/gravitino/dto/responses/ErrorConstants.java
+++
b/common/src/main/java/org/apache/gravitino/dto/responses/ErrorConstants.java
@@ -57,6 +57,9 @@ public class ErrorConstants {
/** Error codes for unauthorized access. */
public static final int UNAUTHORIZED_CODE = 1011;
+ /** Error codes for optimistic-lock conflicts. */
+ public static final int OPTIMISTIC_LOCK_CONFLICT_CODE = 1012;
+
/** Error codes for invalid state. */
public static final int UNKNOWN_ERROR_CODE = 1100;
diff --git
a/common/src/main/java/org/apache/gravitino/dto/responses/ErrorResponse.java
b/common/src/main/java/org/apache/gravitino/dto/responses/ErrorResponse.java
index a47d4cbb80..df5afc81a4 100644
--- a/common/src/main/java/org/apache/gravitino/dto/responses/ErrorResponse.java
+++ b/common/src/main/java/org/apache/gravitino/dto/responses/ErrorResponse.java
@@ -234,6 +234,20 @@ public class ErrorResponse extends BaseResponse {
ErrorConstants.ALREADY_EXISTS_CODE, type, message,
getStackTrace(throwable));
}
+ /**
+ * Create a new optimistic-lock conflict error instance of {@link
ErrorResponse}.
+ *
+ * @param type The type of the error.
+ * @param message The message of the error.
+ * @param throwable The throwable that caused the error.
+ * @return The new instance.
+ */
+ public static ErrorResponse optimisticLockConflict(
+ String type, String message, Throwable throwable) {
+ return new ErrorResponse(
+ ErrorConstants.OPTIMISTIC_LOCK_CONFLICT_CODE, type, message,
getStackTrace(throwable));
+ }
+
/**
* Create a new not in use error instance of {@link ErrorResponse}.
*
diff --git
a/common/src/test/java/org/apache/gravitino/dto/responses/TestResponses.java
b/common/src/test/java/org/apache/gravitino/dto/responses/TestResponses.java
index 89269ab966..67f2c43659 100644
--- a/common/src/test/java/org/apache/gravitino/dto/responses/TestResponses.java
+++ b/common/src/test/java/org/apache/gravitino/dto/responses/TestResponses.java
@@ -244,6 +244,16 @@ public class TestResponses {
error.validate(); // No exception thrown
}
+ @Test
+ void testOptimisticLockConflictErrorResponse() throws
IllegalArgumentException {
+ ErrorResponse error =
+ ErrorResponse.optimisticLockConflict(
+ "OptimisticLockException", "optimistic lock conflict", null);
+ error.validate(); // No exception thrown
+ assertEquals(ErrorConstants.OPTIMISTIC_LOCK_CONFLICT_CODE,
error.getCode());
+ assertEquals("OptimisticLockException", error.getType());
+ }
+
@Test
void testNonEmptyErrorResponse() throws IllegalArgumentException {
ErrorResponse error = ErrorResponse.nonEmpty("error type", "non empty
error");
diff --git
a/server-common/src/main/java/org/apache/gravitino/server/web/Utils.java
b/server-common/src/main/java/org/apache/gravitino/server/web/Utils.java
index 4ef45b6a50..21338047d6 100644
--- a/server-common/src/main/java/org/apache/gravitino/server/web/Utils.java
+++ b/server-common/src/main/java/org/apache/gravitino/server/web/Utils.java
@@ -136,6 +136,22 @@ public class Utils {
.build();
}
+ /**
+ * Creates an HTTP conflict response for a failed optimistic-lock update.
+ *
+ * @param message the conflict message
+ * @param throwable the conflict exception
+ * @return the HTTP conflict response
+ */
+ public static Response optimisticLockConflict(String message, Throwable
throwable) {
+ return Response.status(Response.Status.CONFLICT)
+ .entity(
+ ErrorResponse.optimisticLockConflict(
+ throwable.getClass().getSimpleName(), message, throwable))
+ .type(MediaType.APPLICATION_JSON)
+ .build();
+ }
+
public static Response notInUse(String message, Throwable throwable) {
return notInUse(throwable.getClass().getSimpleName(), message, throwable);
}
diff --git
a/server-common/src/test/java/org/apache/gravitino/server/web/TestUtils.java
b/server-common/src/test/java/org/apache/gravitino/server/web/TestUtils.java
index c495a6275b..3f276c8316 100644
--- a/server-common/src/test/java/org/apache/gravitino/server/web/TestUtils.java
+++ b/server-common/src/test/java/org/apache/gravitino/server/web/TestUtils.java
@@ -30,7 +30,9 @@ import javax.ws.rs.core.Response;
import org.apache.gravitino.audit.FilesetAuditConstants;
import org.apache.gravitino.audit.FilesetDataOperation;
import org.apache.gravitino.audit.InternalClientType;
+import org.apache.gravitino.dto.responses.ErrorConstants;
import org.apache.gravitino.dto.responses.ErrorResponse;
+import org.apache.gravitino.exceptions.OptimisticLockException;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
@@ -164,6 +166,20 @@ public class TestUtils {
assertEquals("New message", errorResponse.getMessage());
}
+ @Test
+ public void testOptimisticLockConflict() {
+ OptimisticLockException exception = new
OptimisticLockException("Conflict");
+ Response response = Utils.optimisticLockConflict("Conflict", exception);
+
+ assertNotNull(response);
+ assertEquals(Response.Status.CONFLICT.getStatusCode(),
response.getStatus());
+ assertEquals(MediaType.APPLICATION_JSON,
response.getMediaType().toString());
+ ErrorResponse errorResponse = (ErrorResponse) response.getEntity();
+ assertEquals(ErrorConstants.OPTIMISTIC_LOCK_CONFLICT_CODE,
errorResponse.getCode());
+ assertEquals(OptimisticLockException.class.getSimpleName(),
errorResponse.getType());
+ assertEquals("Conflict", errorResponse.getMessage());
+ }
+
@Test
public void testUnsupportedOperation() {
Response response = Utils.unsupportedOperation("Unsupported operation");
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/rest/ExceptionHandlers.java
b/server/src/main/java/org/apache/gravitino/server/web/rest/ExceptionHandlers.java
index 6b550fd869..a3fb6e0a99 100644
---
a/server/src/main/java/org/apache/gravitino/server/web/rest/ExceptionHandlers.java
+++
b/server/src/main/java/org/apache/gravitino/server/web/rest/ExceptionHandlers.java
@@ -43,6 +43,7 @@ import
org.apache.gravitino.exceptions.NonEmptyMetalakeException;
import org.apache.gravitino.exceptions.NonEmptySchemaException;
import org.apache.gravitino.exceptions.NotFoundException;
import org.apache.gravitino.exceptions.NotInUseException;
+import org.apache.gravitino.exceptions.OptimisticLockException;
import org.apache.gravitino.exceptions.PartitionAlreadyExistsException;
import org.apache.gravitino.exceptions.PolicyAlreadyAssociatedException;
import org.apache.gravitino.exceptions.PolicyAlreadyExistsException;
@@ -1105,6 +1106,11 @@ public class ExceptionHandlers {
return Utils.connectionFailed(errorMsg, e);
}
+ if (e instanceof OptimisticLockException) {
+ LOG.warn(errorMsg, e);
+ return Utils.optimisticLockConflict(errorMsg, e);
+ }
+
LOG.error(errorMsg, e);
return Utils.internalError(errorMsg, e);
}
diff --git
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestExceptionHandlers.java
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestExceptionHandlers.java
index bdc43c4fc2..ae5401af36 100644
---
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestExceptionHandlers.java
+++
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestExceptionHandlers.java
@@ -18,6 +18,10 @@
*/
package org.apache.gravitino.server.web.rest;
+import javax.ws.rs.core.Response;
+import org.apache.gravitino.dto.responses.ErrorConstants;
+import org.apache.gravitino.dto.responses.ErrorResponse;
+import org.apache.gravitino.exceptions.OptimisticLockException;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -50,4 +54,19 @@ public class TestExceptionHandlers {
String msg6 = ExceptionHandlers.BaseExceptionHandler.getErrorMsg(e6);
Assertions.assertEquals("", msg6);
}
+
+ @Test
+ public void testOptimisticLockConflictReturnsConflict() {
+ Response response =
+ ExceptionHandlers.handleTableException(
+ OperationType.ALTER,
+ "table",
+ "schema",
+ new OptimisticLockException("The table was modified
concurrently"));
+
+ Assertions.assertEquals(Response.Status.CONFLICT.getStatusCode(),
response.getStatus());
+ ErrorResponse errorResponse = (ErrorResponse) response.getEntity();
+ Assertions.assertEquals(ErrorConstants.OPTIMISTIC_LOCK_CONFLICT_CODE,
errorResponse.getCode());
+ Assertions.assertEquals(OptimisticLockException.class.getSimpleName(),
errorResponse.getType());
+ }
}