Skip to content

Commit c4bd891

Browse files
committed
fix: allow CheckFailure with no failed indexes
The denokv self-hosted implementation does not return indexes of failed checks, so we need to allow this. The spec does not say that servers MUST report indexes of failed checks, only that clients SHOULD report failed indexes. See: denoland/denokv#110
1 parent 5e4a2ee commit c4bd891

File tree

2 files changed

+44
-28
lines changed

2 files changed

+44
-28
lines changed

Diff for: src/denokv/datapath.py

+12-17
Original file line numberDiff line numberDiff line change
@@ -202,18 +202,19 @@ class CheckFailure(DataPathDenoKvError):
202202

203203
all_checks: tuple[Check, ...]
204204
"""All of the Checks sent with the AtomicWrite."""
205-
failed_check_indexes: AbstractSet[int]
205+
failed_check_indexes: AbstractSet[int] | None
206206
"""
207207
The indexes of Checks in all_checks keys whose versionstamp check failed.
208208
209-
The set is sorted with ascending iteration order.
209+
The set is sorted with ascending iteration order. Will be None if the
210+
database does not support reporting which checks failed.
210211
"""
211212

212213
def __init__(
213214
self,
214215
message: str,
215216
all_checks: Iterable[Check],
216-
failed_check_indexes: Iterable[int],
217+
failed_check_indexes: Iterable[int] | None,
217218
*args: object,
218219
endpoint: EndpointInfo,
219220
) -> None:
@@ -222,12 +223,15 @@ def __init__(
222223
self.all_checks = tuple(all_checks)
223224
if len(self.all_checks) == 0:
224225
raise ValueError("all_checks is empty")
225-
ordered_indexes = sorted(failed_check_indexes)
226-
if len(ordered_indexes) == 0:
227-
raise ValueError("failed_check_indexes is empty")
228-
if ordered_indexes[0] < 0 or ordered_indexes[-1] >= len(self.all_checks):
226+
227+
ordered_indexes = sorted(failed_check_indexes) if failed_check_indexes else []
228+
if len(ordered_indexes) > 0 and (
229+
ordered_indexes[0] < 0 or ordered_indexes[-1] >= len(self.all_checks)
230+
):
229231
raise IndexError("failed_check_indexes contains out-of-bounds index")
230-
self.failed_check_indexes = {i: True for i in ordered_indexes}.keys()
232+
self.failed_check_indexes = (
233+
{i: True for i in ordered_indexes}.keys() if ordered_indexes else None
234+
)
231235

232236

233237
DataPathError: TypeAlias = Union[
@@ -539,15 +543,6 @@ async def atomic_write(
539543
)
540544
err.__cause__ = e
541545
return Err(err)
542-
except ValueError as e:
543-
err = ProtocolViolation(
544-
"Server responded to Data Path Atomic Write with "
545-
"CHECK_FAILURE containing no failed checks",
546-
data=write_output,
547-
endpoint=endpoint,
548-
)
549-
err.__cause__ = e
550-
return Err(err)
551546
elif write_output.status == AtomicWriteStatus.AW_WRITE_DISABLED:
552547
return Err(
553548
EndpointNotUsable(

Diff for: test/test_datapath.py

+32-11
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
from denokv._kv_values import VersionStamp
4242
from denokv._pycompat.typing import Awaitable
4343
from denokv._pycompat.typing import Callable
44+
from denokv._pycompat.typing import Iterable
4445
from denokv._pycompat.typing import Mapping
4546
from denokv._pycompat.typing import Sequence
4647
from denokv._pycompat.typing import TypeAlias
@@ -261,7 +262,10 @@ async def violation_atomic_write_check_failure_with_out_of_bounds_index(
261262
).SerializeToString(),
262263
)
263264

264-
async def violation_atomic_write_check_failure_without_failed_checks(
265+
# The denokv self-hosted implementation does not return indexes of failed
266+
# checks.
267+
# https://github.com/denoland/denokv/issues/110
268+
async def quirk_atomic_write_check_failure_without_failed_checks(
265269
request: web.Request,
266270
) -> web.Response:
267271
write = AtomicWrite()
@@ -363,7 +367,7 @@ def add_datapath_post(app: web.Application, path: str, handler: Handler) -> None
363367
)
364368
app.router.add_post(
365369
"/check_failure_without_failed_checks/atomic_write",
366-
violation_atomic_write_check_failure_without_failed_checks,
370+
quirk_atomic_write_check_failure_without_failed_checks,
367371
)
368372
app.router.add_post("/unusable/atomic_write", unusable_atomic_write)
369373
app.router.add_post(
@@ -781,10 +785,12 @@ async def test_atomic_write__raises_when_given_endpoint_without_strong_consisten
781785
),
782786
(
783787
"/check_failure_without_failed_checks",
784-
lambda endpoint: ProtocolViolation(
785-
"Server responded to Data Path Atomic Write with CHECK_FAILURE "
786-
"containing no failed checks",
787-
data=AtomicWriteOutput(status=AtomicWriteStatus.AW_CHECK_FAILURE),
788+
lambda endpoint: CheckFailure(
789+
"Not all checks required by the Atomic Write passed",
790+
all_checks=[
791+
Check(key=pack_key(("x",)), versionstamp=bytes(VersionStamp(0)))
792+
],
793+
failed_check_indexes=[],
788794
endpoint=endpoint,
789795
),
790796
),
@@ -1385,6 +1391,26 @@ def test_CheckFailure(example_endpoint: EndpointInfo) -> None:
13851391
assert msg in str(e)
13861392

13871393

1394+
@pytest.mark.parametrize("failed_check_indexes", [None, ()])
1395+
def test_CheckFailure__failed_check_indexes_is_None_when_no_indexes(
1396+
failed_check_indexes: Iterable[int] | None, example_endpoint: EndpointInfo
1397+
) -> None:
1398+
checks = [
1399+
Check(key=bytes(KvKey(f"a{i}")), versionstamp=bytes(VersionStamp(i)))
1400+
for i in range(4)
1401+
]
1402+
# Failed_check_indexes can be empty (the self-hosted sqlite implementation
1403+
# does not return the indexes of failed checks).
1404+
e = CheckFailure(
1405+
"Foo",
1406+
all_checks=iter(checks),
1407+
failed_check_indexes=failed_check_indexes,
1408+
endpoint=example_endpoint,
1409+
)
1410+
assert e.all_checks == tuple(checks)
1411+
assert e.failed_check_indexes is None
1412+
1413+
13881414
def test_CheckFailure__validates_constructor_args(
13891415
example_endpoint: EndpointInfo,
13901416
) -> None:
@@ -1395,11 +1421,6 @@ def test_CheckFailure__validates_constructor_args(
13951421
"Foo", all_checks=[], failed_check_indexes=[], endpoint=example_endpoint
13961422
)
13971423

1398-
with pytest.raises(ValueError, match=r"failed_check_indexes is empty"):
1399-
CheckFailure(
1400-
"Foo", all_checks=checks, failed_check_indexes=[], endpoint=example_endpoint
1401-
)
1402-
14031424
with pytest.raises(
14041425
IndexError, match=r"failed_check_indexes contains out-of-bounds index"
14051426
):

0 commit comments

Comments
 (0)