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 e5301b00b8 [#12207][#13482] fix(client): Handle conflict and
unauthorized errors (#13474)
e5301b00b8 is described below
commit e5301b00b8cbcf67e078e5486fbf669d113bf82b
Author: Qi Yu <[email protected]>
AuthorDate: Wed Sep 23 23:43:08 2026 +0800
[#12207][#13482] fix(client): Handle conflict and unauthorized errors
(#13474)
### What changes were proposed in this pull request?
- Map optimistic-lock conflict code `1012` to `OptimisticLockException`
in the Java and Python clients, with handler tests and retry guidance.
- Map REST unauthorized code `1011` to the existing Python
`UnauthorizedException`. Let specialized Python handlers forward
unfamiliar codes to the common handler instead of raising `ValueError`.
### Why are the changes needed?
Concurrent metadata changes currently surface as generic REST errors,
preventing callers from handling optimistic-lock conflicts explicitly. A
server authentication failure returns `1011`, but the Python client can
raise `ValueError` while converting that code to an enum.
Fixed: #12207
Fixed: #13482
### Does this PR introduce _any_ user-facing change?
Java and Python callers can catch `OptimisticLockException` for code
`1012`. Python callers can catch `UnauthorizedException` for code
`1011`; unfamiliar server error codes fall back to `RESTException` in
the affected handlers.
### How was this patch tested?
- `./gradlew :clients:client-java:test --tests
org.apache.gravitino.client.TestErrorHandlers -PskipITs`
- Python `test_error_handler` suite: 19 tests passed, including an HTTP
401 response and codes `1011`, `1012`, and `1999`.
- Related Python table, view, and statistics client suites: 21 tests
passed.
- Verified all 21 Python REST handlers for codes `1011`, `1012`, and
`1999`; Python Black and `git diff --check` passed.
---
.../org/apache/gravitino/client/ErrorHandlers.java | 4 +
.../apache/gravitino/client/TestErrorHandlers.java | 59 +++++++++++
clients/client-python/gravitino/constants/error.py | 10 ++
clients/client-python/gravitino/exceptions/base.py | 4 +
.../exceptions/handlers/partition_error_handler.py | 14 +--
.../handlers/statistics_error_handler.py | 3 +-
.../exceptions/handlers/table_error_handler.py | 16 +--
.../exceptions/handlers/view_error_handler.py | 16 +--
.../tests/unittests/test_error_handler.py | 118 +++++++++++++++++++++
docs/how-to-use-gravitino-client.md | 20 ++++
10 files changed, 240 insertions(+), 24 deletions(-)
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/ErrorHandlers.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/ErrorHandlers.java
index 7788a1d44c..7357492245 100644
---
a/clients/client-java/src/main/java/org/apache/gravitino/client/ErrorHandlers.java
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/ErrorHandlers.java
@@ -73,6 +73,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;
@@ -1423,6 +1424,9 @@ public class ErrorHandlers {
@Override
public void accept(ErrorResponse errorResponse) {
+ if (errorResponse.getCode() ==
ErrorConstants.OPTIMISTIC_LOCK_CONFLICT_CODE) {
+ throw new OptimisticLockException("%s",
formatErrorMessage(errorResponse));
+ }
if (errorResponse.getCode() == ErrorConstants.CONNECTION_FAILED_CODE) {
throw new ConnectionFailedException("%s",
formatErrorMessage(errorResponse));
}
diff --git
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestErrorHandlers.java
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestErrorHandlers.java
new file mode 100644
index 0000000000..0416faca8c
--- /dev/null
+++
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestErrorHandlers.java
@@ -0,0 +1,59 @@
+/*
+ * 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.client;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+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;
+
+/** Tests REST error handling for optimistic-lock conflicts. */
+public class TestErrorHandlers {
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testOptimisticLockConflictAcrossHandlers() throws
ReflectiveOperationException {
+ ErrorResponse response =
+ ErrorResponse.optimisticLockConflict(
+ OptimisticLockException.class.getSimpleName(), "Concurrent
update", null);
+ List<Method> factories =
+ Arrays.stream(ErrorHandlers.class.getDeclaredMethods())
+ .filter(method -> Modifier.isPublic(method.getModifiers()))
+ .filter(method -> Modifier.isStatic(method.getModifiers()))
+ .filter(method -> method.getName().endsWith("ErrorHandler"))
+ .filter(method -> method.getParameterCount() == 0)
+ .filter(method ->
Consumer.class.isAssignableFrom(method.getReturnType()))
+ .collect(Collectors.toList());
+ Assertions.assertFalse(factories.isEmpty());
+
+ for (Method factory : factories) {
+ Consumer<ErrorResponse> handler = (Consumer<ErrorResponse>)
factory.invoke(null);
+ OptimisticLockException exception =
+ Assertions.assertThrows(
+ OptimisticLockException.class, () -> handler.accept(response),
factory.getName());
+ Assertions.assertEquals("Concurrent update", exception.getMessage(),
factory.getName());
+ }
+ }
+}
diff --git a/clients/client-python/gravitino/constants/error.py
b/clients/client-python/gravitino/constants/error.py
index 27e6031451..c63b8a9d09 100644
--- a/clients/client-python/gravitino/constants/error.py
+++ b/clients/client-python/gravitino/constants/error.py
@@ -29,6 +29,8 @@ from gravitino.exceptions.base import (
ForbiddenException,
NotInUseException,
InUseException,
+ OptimisticLockException,
+ UnauthorizedException,
)
@@ -68,6 +70,12 @@ class ErrorConstants(IntEnum):
# Error codes for drop an in use entity.
IN_USE_CODE = 1010
+ # Error codes for unauthorized access.
+ UNAUTHORIZED_CODE = 1011
+
+ # Error codes for optimistic-lock conflicts.
+ OPTIMISTIC_LOCK_CONFLICT_CODE = 1012
+
# Error codes for invalid state.
UNKNOWN_ERROR_CODE = 1100
@@ -84,6 +92,8 @@ EXCEPTION_MAPPING = {
ForbiddenException: ErrorConstants.FORBIDDEN_CODE,
NotInUseException: ErrorConstants.NOT_IN_USE_CODE,
InUseException: ErrorConstants.IN_USE_CODE,
+ OptimisticLockException: ErrorConstants.OPTIMISTIC_LOCK_CONFLICT_CODE,
+ UnauthorizedException: ErrorConstants.UNAUTHORIZED_CODE,
}
ERROR_CODE_MAPPING = {v: k for k, v in EXCEPTION_MAPPING.items()}
diff --git a/clients/client-python/gravitino/exceptions/base.py
b/clients/client-python/gravitino/exceptions/base.py
index b22c59c443..42c0f91168 100644
--- a/clients/client-python/gravitino/exceptions/base.py
+++ b/clients/client-python/gravitino/exceptions/base.py
@@ -157,6 +157,10 @@ class ConnectionFailedException(GravitinoRuntimeException):
"""An exception thrown when connect to catalog failed."""
+class OptimisticLockException(GravitinoRuntimeException):
+ """Raised when another update changes an entity before a write
completes."""
+
+
class UnauthorizedException(GravitinoRuntimeException):
"""An exception thrown when a user is not authorized to perform an
action."""
diff --git
a/clients/client-python/gravitino/exceptions/handlers/partition_error_handler.py
b/clients/client-python/gravitino/exceptions/handlers/partition_error_handler.py
index 52fb36c3e7..0d5b921ea2 100644
---
a/clients/client-python/gravitino/exceptions/handlers/partition_error_handler.py
+++
b/clients/client-python/gravitino/exceptions/handlers/partition_error_handler.py
@@ -35,13 +35,13 @@ from gravitino.exceptions.handlers.rest_error_handler
import RestErrorHandler
class PartitionErrorHandler(RestErrorHandler):
def handle(self, error_response: ErrorResponse):
error_message = error_response.format_error_message()
- code = ErrorConstants(error_response.code())
+ code = error_response.code()
exception_type = error_response.type()
- if code is ErrorConstants.ILLEGAL_ARGUMENTS_CODE:
+ if code == ErrorConstants.ILLEGAL_ARGUMENTS_CODE:
raise IllegalArgumentException(error_message)
- if code is ErrorConstants.NOT_FOUND_CODE:
+ if code == ErrorConstants.NOT_FOUND_CODE:
if exception_type == NoSuchSchemaException.__name__:
raise NoSuchSchemaException(error_message)
if exception_type == NoSuchTableException.__name__:
@@ -50,16 +50,16 @@ class PartitionErrorHandler(RestErrorHandler):
raise NoSuchPartitionException(error_message)
raise NotFoundException(error_message)
- if code is ErrorConstants.ALREADY_EXISTS_CODE:
+ if code == ErrorConstants.ALREADY_EXISTS_CODE:
raise PartitionAlreadyExistsException(error_message)
- if code is ErrorConstants.INTERNAL_ERROR_CODE:
+ if code == ErrorConstants.INTERNAL_ERROR_CODE:
raise RuntimeError(error_message)
- if code is ErrorConstants.UNSUPPORTED_OPERATION_CODE:
+ if code == ErrorConstants.UNSUPPORTED_OPERATION_CODE:
raise UnsupportedOperationException(error_message)
- if code is ErrorConstants.NOT_IN_USE_CODE:
+ if code == ErrorConstants.NOT_IN_USE_CODE:
if exception_type == CatalogNotInUseException.__name__:
raise CatalogNotInUseException(error_message)
if exception_type == MetalakeNotInUseException.__name__:
diff --git
a/clients/client-python/gravitino/exceptions/handlers/statistics_error_handler.py
b/clients/client-python/gravitino/exceptions/handlers/statistics_error_handler.py
index c2c8a51ab3..c453b66205 100644
---
a/clients/client-python/gravitino/exceptions/handlers/statistics_error_handler.py
+++
b/clients/client-python/gravitino/exceptions/handlers/statistics_error_handler.py
@@ -39,7 +39,8 @@ class StatisticsErrorHandler(RestErrorHandler):
self, error_response: ErrorResponse
): # pylint: disable=too-many-branches
error_message = error_response.format_error_message()
- code = ErrorConstants(error_response.code())
+ # Keep unknown server codes intact so the common handler can report
them.
+ code = error_response.code()
exception_type = error_response.type()
match code:
diff --git
a/clients/client-python/gravitino/exceptions/handlers/table_error_handler.py
b/clients/client-python/gravitino/exceptions/handlers/table_error_handler.py
index 85c4846ecf..f3b16cbed9 100644
--- a/clients/client-python/gravitino/exceptions/handlers/table_error_handler.py
+++ b/clients/client-python/gravitino/exceptions/handlers/table_error_handler.py
@@ -35,32 +35,32 @@ from gravitino.exceptions.handlers.rest_error_handler
import RestErrorHandler
class TableErrorHandler(RestErrorHandler):
def handle(self, error_response: ErrorResponse):
error_message = error_response.format_error_message()
- code = ErrorConstants(error_response.code())
+ code = error_response.code()
exception_type = error_response.type()
- if code is ErrorConstants.ILLEGAL_ARGUMENTS_CODE:
+ if code == ErrorConstants.ILLEGAL_ARGUMENTS_CODE:
raise IllegalArgumentException(error_message)
- if code is ErrorConstants.NOT_FOUND_CODE:
+ if code == ErrorConstants.NOT_FOUND_CODE:
if exception_type == NoSuchSchemaException.__name__:
raise NoSuchSchemaException(error_message)
if exception_type == NoSuchTableException.__name__:
raise NoSuchTableException(error_message)
raise NotFoundException(error_message)
- if code is ErrorConstants.ALREADY_EXISTS_CODE:
+ if code == ErrorConstants.ALREADY_EXISTS_CODE:
raise TableAlreadyExistsException(error_message)
- if code is ErrorConstants.INTERNAL_ERROR_CODE:
+ if code == ErrorConstants.INTERNAL_ERROR_CODE:
raise RuntimeError(error_message)
- if code is ErrorConstants.UNSUPPORTED_OPERATION_CODE:
+ if code == ErrorConstants.UNSUPPORTED_OPERATION_CODE:
raise UnsupportedOperationException(error_message)
- if code is ErrorConstants.FORBIDDEN_CODE:
+ if code == ErrorConstants.FORBIDDEN_CODE:
raise ForbiddenException(error_message)
- if code is ErrorConstants.NOT_IN_USE_CODE:
+ if code == ErrorConstants.NOT_IN_USE_CODE:
if exception_type == CatalogNotInUseException.__name__:
raise CatalogNotInUseException(error_message)
if exception_type == MetalakeNotInUseException.__name__:
diff --git
a/clients/client-python/gravitino/exceptions/handlers/view_error_handler.py
b/clients/client-python/gravitino/exceptions/handlers/view_error_handler.py
index 674438c144..a40632bbcf 100644
--- a/clients/client-python/gravitino/exceptions/handlers/view_error_handler.py
+++ b/clients/client-python/gravitino/exceptions/handlers/view_error_handler.py
@@ -38,13 +38,13 @@ class ViewErrorHandler(RestErrorHandler):
def handle(self, error_response: ErrorResponse):
error_message = error_response.format_error_message()
- code = ErrorConstants(error_response.code())
+ code = error_response.code()
exception_type = error_response.type()
- if code is ErrorConstants.ILLEGAL_ARGUMENTS_CODE:
+ if code == ErrorConstants.ILLEGAL_ARGUMENTS_CODE:
raise IllegalArgumentException(error_message)
- if code is ErrorConstants.NOT_FOUND_CODE:
+ if code == ErrorConstants.NOT_FOUND_CODE:
if exception_type == NoSuchCatalogException.__name__:
raise NoSuchCatalogException(error_message)
if exception_type == NoSuchSchemaException.__name__:
@@ -53,19 +53,19 @@ class ViewErrorHandler(RestErrorHandler):
raise NoSuchViewException(error_message)
raise NotFoundException(error_message)
- if code is ErrorConstants.ALREADY_EXISTS_CODE:
+ if code == ErrorConstants.ALREADY_EXISTS_CODE:
raise ViewAlreadyExistsException(error_message)
- if code is ErrorConstants.INTERNAL_ERROR_CODE:
+ if code == ErrorConstants.INTERNAL_ERROR_CODE:
raise RuntimeError(error_message)
- if code is ErrorConstants.UNSUPPORTED_OPERATION_CODE:
+ if code == ErrorConstants.UNSUPPORTED_OPERATION_CODE:
raise UnsupportedOperationException(error_message)
- if code is ErrorConstants.FORBIDDEN_CODE:
+ if code == ErrorConstants.FORBIDDEN_CODE:
raise ForbiddenException(error_message)
- if code is ErrorConstants.NOT_IN_USE_CODE:
+ if code == ErrorConstants.NOT_IN_USE_CODE:
if exception_type == CatalogNotInUseException.__name__:
raise CatalogNotInUseException(error_message)
if exception_type == MetalakeNotInUseException.__name__:
diff --git a/clients/client-python/tests/unittests/test_error_handler.py
b/clients/client-python/tests/unittests/test_error_handler.py
index 778581450e..653f0c9708 100644
--- a/clients/client-python/tests/unittests/test_error_handler.py
+++ b/clients/client-python/tests/unittests/test_error_handler.py
@@ -16,6 +16,9 @@
# under the License.
import unittest
+from io import BytesIO
+from unittest.mock import patch
+from urllib.error import HTTPError
from gravitino.dto.responses.error_response import ErrorResponse
from gravitino.exceptions.base import (
@@ -28,6 +31,7 @@ from gravitino.exceptions.base import (
IllegalMetadataObjectException,
IllegalPrivilegeException,
IllegalRoleException,
+ IllegalStatisticNameException,
InternalError,
MetalakeAlreadyExistsException,
MetalakeNotInUseException,
@@ -47,11 +51,13 @@ from gravitino.exceptions.base import (
NotEmptyException,
NotFoundException,
NotInUseException,
+ OptimisticLockException,
PartitionAlreadyExistsException,
RESTException,
RoleAlreadyExistsException,
SchemaAlreadyExistsException,
TableAlreadyExistsException,
+ UnauthorizedException,
UnsupportedOperationException,
UserAlreadyExistsException,
ViewAlreadyExistsException,
@@ -68,6 +74,9 @@ from gravitino.exceptions.handlers.partition_error_handler
import (
PARTITION_ERROR_HANDLER,
)
from gravitino.exceptions.handlers.rest_error_handler import REST_ERROR_HANDLER
+from gravitino.exceptions.handlers.statistics_error_handler import (
+ STATISTICS_ERROR_HANDLER,
+)
from gravitino.exceptions.handlers.permission_error_handler import (
PERMISSION_ERROR_HANDLER,
)
@@ -76,9 +85,118 @@ from gravitino.exceptions.handlers.schema_error_handler
import SCHEMA_ERROR_HAND
from gravitino.exceptions.handlers.table_error_handler import
TABLE_ERROR_HANDLER
from gravitino.exceptions.handlers.user_error_handler import USER_ERROR_HANDLER
from gravitino.exceptions.handlers.view_error_handler import VIEW_ERROR_HANDLER
+from gravitino.utils.http_client import HTTPClient
class TestErrorHandler(unittest.TestCase):
+ def test_http_unauthorized_response(self):
+ body = (
+ b'{"code":1011,"type":"UnauthorizedException",'
+ b'"message":"Authentication failed","stack":null}'
+ )
+ with patch("gravitino.utils.http_client.build_opener") as build_opener:
+ build_opener.return_value.open.side_effect = HTTPError(
+ "http://localhost:8090/api/test",
+ 401,
+ "Unauthorized",
+ None,
+ BytesIO(body),
+ )
+ with self.assertRaisesRegex(UnauthorizedException, "Authentication
failed"):
+ HTTPClient("http://localhost:8090").get(
+ "/api/test", error_handler=TABLE_ERROR_HANDLER
+ )
+
+ def test_unauthorized_error_code_across_handlers(self):
+ response = ErrorResponse.from_json(
+ '{"code":1011,"type":"UnauthorizedException",'
+ '"message":"Authentication failed","stack":null}'
+ )
+ generated = ErrorResponse.generate_error_response(
+ UnauthorizedException, "Authentication failed"
+ )
+ self.assertEqual(1011, generated.code())
+
+ for handler in (
+ REST_ERROR_HANDLER,
+ TABLE_ERROR_HANDLER,
+ VIEW_ERROR_HANDLER,
+ PARTITION_ERROR_HANDLER,
+ STATISTICS_ERROR_HANDLER,
+ CATALOG_ERROR_HANDLER,
+ ):
+ with self.subTest(handler=type(handler).__name__):
+ with self.assertRaisesRegex(
+ UnauthorizedException, "Authentication failed"
+ ):
+ handler.handle(response)
+
+ def test_unrecognized_error_code_across_specialized_handlers(self):
+ response = ErrorResponse.from_json(
+ '{"code":1999,"type":"FutureServerException",'
+ '"message":"Future server error","stack":null}'
+ )
+ for handler in (
+ TABLE_ERROR_HANDLER,
+ VIEW_ERROR_HANDLER,
+ PARTITION_ERROR_HANDLER,
+ STATISTICS_ERROR_HANDLER,
+ ):
+ with self.subTest(handler=type(handler).__name__):
+ with self.assertRaisesRegex(RESTException, "Future server
error"):
+ handler.handle(response)
+
+ def test_statistics_handler_keeps_specific_errors(self):
+ response = ErrorResponse.from_json(
+ '{"code":1001,"type":"IllegalArgumentException",'
+ '"message":"Invalid statistic","stack":null}'
+ )
+ with self.assertRaisesRegex(IllegalArgumentException, "Invalid
statistic"):
+ STATISTICS_ERROR_HANDLER.handle(response)
+
+ specific_response = ErrorResponse.from_json(
+ '{"code":1001,"type":"IllegalStatisticNameException",'
+ '"message":"Invalid statistic name","stack":null}'
+ )
+ with self.assertRaisesRegex(
+ IllegalStatisticNameException, "Invalid statistic name"
+ ):
+ STATISTICS_ERROR_HANDLER.handle(specific_response)
+
+ def test_statistics_handler_forwards_unrecognized_codes(self):
+ for code, exception in ((1011, UnauthorizedException), (1999,
RESTException)):
+ with self.subTest(code=code):
+ response = ErrorResponse.from_json(
+ f'{{"code":{code},"type":"UnexpectedError",'
+ '"message":"Server error","stack":null}'
+ )
+ with self.assertRaisesRegex(exception, "Server error"):
+ STATISTICS_ERROR_HANDLER.handle(response)
+
+ def test_optimistic_lock_conflict(self):
+ response = ErrorResponse.from_json(
+ '{"code":1012,"type":"OptimisticLockException",'
+ '"message":"Concurrent update","stack":null}'
+ )
+ generated = ErrorResponse.generate_error_response(
+ OptimisticLockException, "Concurrent update"
+ )
+ self.assertEqual(1012, generated.code())
+
+ for handler in (
+ REST_ERROR_HANDLER,
+ TABLE_ERROR_HANDLER,
+ VIEW_ERROR_HANDLER,
+ PARTITION_ERROR_HANDLER,
+ STATISTICS_ERROR_HANDLER,
+ CATALOG_ERROR_HANDLER,
+ ):
+ with self.subTest(handler=type(handler).__name__):
+ with self.assertRaisesRegex(
+ OptimisticLockException, "Concurrent update"
+ ):
+ handler.handle(response)
+
def test_rest_error_handler(self):
with self.assertRaises(RESTException):
REST_ERROR_HANDLER.handle(
diff --git a/docs/how-to-use-gravitino-client.md
b/docs/how-to-use-gravitino-client.md
index fdc690afc8..4bcc3bf335 100644
--- a/docs/how-to-use-gravitino-client.md
+++ b/docs/how-to-use-gravitino-client.md
@@ -72,3 +72,23 @@ gravitino_client = GravitinoClient(
| `gravitino_client_request_timeout` | An optional client timeout in seconds.
| `10` | No |
**Note:** Invalid configuration properties will result in exceptions.
+
+## Retrying concurrent metadata changes
+
+If another writer changes metadata during an alter or drop, the server returns
HTTP 409
+with error code `1012`. The Java and Python clients raise
`OptimisticLockException`.
+Import the exception in Java:
+
+```java
+import org.apache.gravitino.exceptions.OptimisticLockException;
+```
+
+Or in Python:
+
+```python
+from gravitino.exceptions.base import OptimisticLockException
+```
+
+Catch this exception, reload the latest metadata, reconsider your intended
change, and
+retry with a bounded number of attempts. Do not replay a stale update
unchanged or
+retry every HTTP 409 response, since other conflicts may require a different
action.