mirror of
https://github.com/lancedb/lancedb.git
synced 2026-11-13 14:16:54 +00:00
fix: avoid retrying remote branch deletions (#4854)
This commit is contained in:
@@ -149,6 +149,8 @@ def _http_error_class(
|
||||
return input_class
|
||||
if error_code == "already_exists":
|
||||
return AlreadyExistsError
|
||||
if status_code == 409:
|
||||
return ConflictError
|
||||
if error_code != "not_found" or resource_kind is None:
|
||||
return None
|
||||
return {
|
||||
@@ -268,6 +270,22 @@ class AlreadyExistsError(HttpError):
|
||||
"""A remote resource already exists."""
|
||||
|
||||
|
||||
class ConflictError(HttpError):
|
||||
"""A remote operation conflicts with the current state of a resource.
|
||||
|
||||
For example, deleting a branch whose files are still needed by another
|
||||
branch raises this error. The server's reason is included in the message.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> error = HttpError("main depends on the branch's files", "req-1", 409)
|
||||
>>> isinstance(error, ConflictError)
|
||||
True
|
||||
>>> error.status_code
|
||||
409
|
||||
"""
|
||||
|
||||
|
||||
class TableNotFoundError(HttpError, ValueError, _NamespaceTableNotFoundError):
|
||||
"""A remote table was not found.
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ from lance_namespace.errors import TableNotFoundError as NamespaceTableNotFoundE
|
||||
from lancedb.db import DBConnection
|
||||
from lancedb.remote.errors import (
|
||||
AlreadyExistsError,
|
||||
ConflictError,
|
||||
FunctionNotFoundError,
|
||||
HttpError,
|
||||
LanceDBClientError,
|
||||
@@ -72,6 +73,20 @@ def test_pickle_http_transport_error():
|
||||
assert restored.request_id == "req-789"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("error_code", [None, "conflict", "future_conflict_code"])
|
||||
def test_http_conflict_error_preserves_reason_and_metadata(error_code):
|
||||
reason = "Conflict: branch 'exp' cannot be deleted, main depends on its files"
|
||||
original = HttpError(reason, "req-conflict", 409, error_code)
|
||||
for error in [original, pickle.loads(pickle.dumps(original))]:
|
||||
assert type(error) is ConflictError
|
||||
assert isinstance(error, HttpError)
|
||||
assert str(error) == reason
|
||||
assert error.request_id == "req-conflict"
|
||||
assert error.status_code == 409
|
||||
assert error.error_code == error_code
|
||||
assert error.is_transport_error is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"error_code,resource_kind,status_code,expected_type",
|
||||
[
|
||||
@@ -251,6 +266,9 @@ def test_sql_errors_use_only_server_codes(code, category, type_name):
|
||||
(400, "schema_mismatch", "SchemaMismatchError"),
|
||||
(404, "table_not_found", "TableNotFoundError"),
|
||||
(404, "reference_not_found", "ReferenceNotFoundError"),
|
||||
(409, None, "ConflictError"),
|
||||
(409, "conflict", "ConflictError"),
|
||||
(500, "conflict", "HttpError"),
|
||||
(500, "column_not_found", "HttpError"),
|
||||
(503, "table_not_found", "HttpError"),
|
||||
],
|
||||
|
||||
@@ -45,6 +45,7 @@ from lancedb.query import (
|
||||
from lancedb.remote import ClientConfig
|
||||
from lancedb.remote.errors import (
|
||||
AlreadyExistsError,
|
||||
ConflictError,
|
||||
HttpError,
|
||||
NamespaceNotFoundError,
|
||||
RetryError,
|
||||
@@ -733,6 +734,43 @@ def test_remote_table_branches_sync():
|
||||
table.branches.delete("exp")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("use_async", [False, True])
|
||||
@pytest.mark.parametrize("json_response", [False, True])
|
||||
async def test_remote_branch_delete_conflict_is_not_retried(use_async, json_response):
|
||||
request_ids = []
|
||||
reason = "Conflict: branch 'exp' cannot be deleted, main depends on its files"
|
||||
|
||||
def handler(request):
|
||||
if request.path == "/v1/table/test/describe/":
|
||||
_branch_open_handler(request)
|
||||
return
|
||||
|
||||
assert request.path == "/v1/table/test/branches/delete/"
|
||||
content_len = int(request.headers["Content-Length"])
|
||||
assert json.loads(request.rfile.read(content_len)) == {"name": "exp"}
|
||||
request_ids.append(request.headers["x-request-id"])
|
||||
request.send_response(409)
|
||||
request.end_headers()
|
||||
body = json.dumps({"code": 14, "error": reason}) if json_response else reason
|
||||
request.wfile.write(body.encode())
|
||||
|
||||
if use_async:
|
||||
async with mock_lancedb_connection_async(handler) as db:
|
||||
table = await db.open_table("test")
|
||||
with pytest.raises(ConflictError, match=re.escape(reason)) as exc_info:
|
||||
await table.branches.delete("exp")
|
||||
else:
|
||||
with mock_lancedb_connection(handler) as db:
|
||||
table = db.open_table("test")
|
||||
with pytest.raises(ConflictError, match=re.escape(reason)) as exc_info:
|
||||
table.branches.delete("exp")
|
||||
|
||||
assert request_ids == [exc_info.value.request_id]
|
||||
assert exc_info.value.status_code == 409
|
||||
assert isinstance(exc_info.value, HttpError)
|
||||
|
||||
|
||||
def test_remote_table_cherry_pick_defaults_to_execute():
|
||||
cherry_pick_bodies = []
|
||||
diff = {
|
||||
|
||||
Reference in New Issue
Block a user