fix: avoid retrying remote branch deletions (#4854)

This commit is contained in:
lancedb-gatefixer[bot]
2026-11-05 13:20:20 -08:00
committed by GitHub
parent a2abfbced7
commit 0d72977186
5 changed files with 154 additions and 3 deletions
+18
View File
@@ -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.
+18
View File
@@ -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"),
],
+38
View File
@@ -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 = {