Skip to content

Commit d284536

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 0521ee6 commit d284536

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
@@ -224,18 +224,19 @@ class CheckFailure(DataPathDenoKvError):
224224

225225
all_checks: tuple[Check, ...]
226226
"""All of the Checks sent with the AtomicWrite."""
227-
failed_check_indexes: AbstractSet[int]
227+
failed_check_indexes: AbstractSet[int] | None
228228
"""
229229
The indexes of Checks in all_checks keys whose versionstamp check failed.
230230
231-
The set is sorted with ascending iteration order.
231+
The set is sorted with ascending iteration order. Will be None if the
232+
database does not support reporting which checks failed.
232233
"""
233234

234235
def __init__(
235236
self,
236237
message: str,
237238
all_checks: Iterable[Check],
238-
failed_check_indexes: Iterable[int],
239+
failed_check_indexes: Iterable[int] | None,
239240
*args: object,
240241
endpoint: EndpointInfo,
241242
) -> None:
@@ -244,12 +245,15 @@ def __init__(
244245
self.all_checks = tuple(all_checks)
245246
if len(self.all_checks) == 0:
246247
raise ValueError("all_checks is empty")
247-
ordered_indexes = sorted(failed_check_indexes)
248-
if len(ordered_indexes) == 0:
249-
raise ValueError("failed_check_indexes is empty")
250-
if ordered_indexes[0] < 0 or ordered_indexes[-1] >= len(self.all_checks):
248+
249+
ordered_indexes = sorted(failed_check_indexes) if failed_check_indexes else []
250+
if len(ordered_indexes) > 0 and (
251+
ordered_indexes[0] < 0 or ordered_indexes[-1] >= len(self.all_checks)
252+
):
251253
raise IndexError("failed_check_indexes contains out-of-bounds index")
252-
self.failed_check_indexes = {i: True for i in ordered_indexes}.keys()
254+
self.failed_check_indexes = (
255+
{i: True for i in ordered_indexes}.keys() if ordered_indexes else None
256+
)
253257

254258

255259
DataPathError: TypeAlias = (
@@ -561,15 +565,6 @@ async def atomic_write(
561565
)
562566
err.__cause__ = e
563567
return Err(err)
564-
except ValueError as e:
565-
err = ProtocolViolation(
566-
"Server responded to Data Path Atomic Write with "
567-
"CHECK_FAILURE containing no failed checks",
568-
data=write_output,
569-
endpoint=endpoint,
570-
)
571-
err.__cause__ = e
572-
return Err(err)
573568
elif write_output.status == AtomicWriteStatus.AW_WRITE_DISABLED:
574569
return Err(
575570
EndpointNotUsable(

Diff for: test/test_datapath.py

+32-11
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import struct
66
from typing import Awaitable
77
from typing import Callable
8+
from typing import Iterable
89
from typing import Literal
910
from typing import Mapping
1011
from typing import Sequence
@@ -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)