Repository navigation
[fix] Raise on missing keys in kv_batch_get and reject duplicate keys before batch writes #196
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,6 +33,7 @@ | |
| import shutil | ||
| import subprocess | ||
| import time | ||
| from collections import Counter | ||
| from importlib import resources | ||
| from pathlib import Path | ||
| from typing import Any, Callable | ||
|
|
@@ -456,6 +457,7 @@ def kv_batch_put( | |
| ValueError: When both ``fields`` and ``tags`` are empty. | ||
| ValueError: When ``fields`` batch size mismatches key count. | ||
| ValueError: When ``tags`` length mismatches key count. | ||
| ValueError: When ``keys`` contains duplicates. | ||
| RuntimeError: When retrieved metadata size mismatches input key count. | ||
|
|
||
| Example: | ||
|
|
@@ -502,7 +504,7 @@ def kv_batch_get_by_meta(meta: KVBatchMeta, select_fields: list[str] | str | Non | |
| TensorDict with the requested data | ||
|
|
||
| Raises: | ||
| ValueError: If keys or partition are not found | ||
| ValueError: If the partition or any of the keys is not found | ||
| ValueError: If empty fields exist in any key (sample) | ||
| ValueError: If any field in select_fields doesn't exist in KVBatchMeta.fields | ||
|
|
||
|
|
@@ -557,17 +559,17 @@ def kv_batch_get( | |
|
|
||
| Only supported by SimpleStorage; KV backends raise ``NotImplementedError``. | ||
| parser_args: Optional per-key arguments for ``data_parser``, aligned with ``keys``. | ||
| Requires ``data_parser``, and every key must exist. They are serialized like | ||
| request data, so tuples arrive as lists. | ||
| Requires ``data_parser``. They are serialized like request data, so tuples | ||
| arrive as lists. | ||
|
|
||
| Returns: | ||
| TensorDict with the requested data | ||
|
|
||
| Raises: | ||
| ValueError: If keys or partition are not found | ||
| ValueError: If the partition or any of the keys is not found | ||
| ValueError: If empty fields exist in any key (sample) | ||
| ValueError: If ``parser_args`` is given without ``data_parser``, does not match the | ||
| number of keys, or some keys are missing | ||
| ValueError: If ``parser_args`` is given without ``data_parser`` or does not match the | ||
| number of keys | ||
| RuntimeError: If ``data_parser`` fails or breaks its contract on a storage unit | ||
|
|
||
| Example: | ||
|
|
@@ -832,6 +834,13 @@ async def async_kv_put( | |
| ) | ||
|
|
||
|
|
||
| def _check_unique_keys(keys: list[str]) -> None: | ||
| # KVBatchMeta rejects duplicates only after the write has landed, so reject them before any RPC. | ||
| duplicates = sorted(key for key, count in Counter(keys).items() if count > 1) | ||
| if duplicates: | ||
| raise ValueError(f"keys must be unique, got duplicates: {duplicates[:20]}") | ||
|
|
||
|
|
||
| async def async_kv_batch_put( | ||
| keys: list[str], | ||
| partition_id: str, | ||
|
|
@@ -869,6 +878,7 @@ async def async_kv_batch_put( | |
| Raises: | ||
| ValueError: If neither `fields` nor `tags` is provided | ||
| ValueError: If length of `keys` doesn't match length of `tags` or the batch_size of `fields` TensorDict | ||
| ValueError: If `keys` contains duplicates | ||
| RuntimeError: If retrieved BatchMeta size doesn't match length of `keys` | ||
|
|
||
| Example: | ||
|
|
@@ -892,6 +902,7 @@ async def async_kv_batch_put( | |
| f"`keys` with length {len(keys)} does not match the `fields` TensorDict with " | ||
| f"batch_size {fields.batch_size[0]}" | ||
| ) | ||
| _check_unique_keys(keys) | ||
|
|
||
| tq_client = _maybe_create_tq_client() | ||
|
|
||
|
|
@@ -947,7 +958,7 @@ async def async_kv_batch_get_by_meta(meta: KVBatchMeta, select_fields: list[str] | |
| TensorDict with the requested data | ||
|
|
||
| Raises: | ||
| ValueError: If keys or partition are not found | ||
| ValueError: If the partition or any of the keys is not found | ||
| ValueError: If empty fields exist in any key (sample) | ||
| ValueError: If any field in select_fields doesn't exist in KVBatchMeta.fields | ||
|
|
||
|
|
@@ -1008,10 +1019,10 @@ async def async_kv_batch_get( | |
| TensorDict with the requested data | ||
|
|
||
| Raises: | ||
| ValueError: If keys or partition are not found | ||
| ValueError: If the partition or any of the keys is not found | ||
| ValueError: If empty fields exist in any key (sample) | ||
| ValueError: If ``parser_args`` is given without ``data_parser``, does not match the | ||
| number of keys, or some keys are missing | ||
| ValueError: If ``parser_args`` is given without ``data_parser`` or does not match the | ||
| number of keys | ||
| RuntimeError: If ``data_parser`` fails or breaks its contract on a storage unit | ||
|
|
||
| Example: | ||
|
|
@@ -1032,10 +1043,14 @@ async def async_kv_batch_get( | |
|
|
||
| if batch_meta.size == 0: | ||
| raise ValueError("keys or partition were not found!") | ||
| # Missing keys are dropped from the meta, which would shift parser_args onto other keys. | ||
| num_keys = 1 if isinstance(keys, str) else len(keys) | ||
| if parser_args is not None and batch_meta.size != num_keys: | ||
| raise ValueError(f"only {batch_meta.size} of {num_keys} keys were found; parser_args needs every key") | ||
| # The controller drops missing keys from the meta, so the rows would no longer line up with keys. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This new rationale is also only one AGENTS.md reference: AGENTS.md:L24-L24 Useful? React with 👍 / 👎. |
||
| requested = [keys] if isinstance(keys, str) else keys | ||
| if batch_meta.size != len(requested): | ||
| found = set(await tq_client.async_kv_retrieve_keys(batch_meta.global_indexes, partition_id)) | ||
| missing = [key for key in requested if key not in found] | ||
| raise ValueError( | ||
| f"{len(missing)} of {len(requested)} keys were not found in partition {partition_id!r}: {missing[:20]}" | ||
| ) | ||
|
|
||
| if select_fields is not None: | ||
| if isinstance(select_fields, str): | ||
|
|
@@ -1178,6 +1193,7 @@ async def async_kv_batch_update( | |
| raise ValueError(f"fields batch size {fields.batch_size} does not match {len(keys)} keys") | ||
| if not callable(merge_fn): | ||
| raise TypeError("merge_fn must be callable") | ||
| _check_unique_keys(keys) | ||
| tq_client = _maybe_create_tq_client() | ||
| batch_meta = await tq_client.async_kv_retrieve_meta(keys=keys, partition_id=partition_id, create=False) | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This explanation is a single
#line, while the root repository rule requires explanatory comments to be 2–4 lines. Rewrite the rationale across the required span so this new helper meets the hard comment standard.AGENTS.md reference: AGENTS.md:L24-L24
Useful? React with 👍 / 👎.