GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
GeezerVison is showing github.com — Merge dev 1029 by 0oshowero0 · Pull Request #94 · TransferQueue/TransferQueue
This repository was archived by the owner on Jan 21, 2026. It is now read-only.
Conversation
|
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. WalkthroughRefactors storage to a single-controller model and introduces a pluggable storage-manager layer. Moves storage code into Changes
Sequence Diagram(s)sequenceDiagram
participant App as App/Trainer
participant Client as AsyncTransferQueueClient
participant Manager as AsyncSimpleStorageManager
participant Controller as TransferQueueController
participant Storage as SimpleStorageUnit[N]
rect rgb(240,248,255)
App->>Client: __init__(client_id, controller_info)
Client->>Controller: HANDSHAKE_REQUEST
Controller-->>Client: HANDSHAKE_ACK
App->>Client: initialize_storage_manager("AsyncSimpleStorageManager", config)
Client->>Manager: __init__(config)
Manager->>Controller: HANDSHAKE_REQUEST
Controller-->>Manager: HANDSHAKE_ACK
end
rect rgb(228,255,228)
App->>Client: async_put(data, metadata)
Client->>Manager: put_data(data, metadata)
Manager->>Manager: build_storage_meta_groups()
loop per storage unit
Manager->>Storage: PUT_DATA_REQUEST
Storage-->>Manager: PUT_DATA_RESPONSE
end
Manager->>Controller: NOTIFY_DATA_UPDATE
Controller-->>Manager: NOTIFY_DATA_UPDATE_ACK
end
rect rgb(255,245,230)
App->>Client: async_get_data(metadata)
Client->>Manager: get_data(metadata)
loop per storage unit
Manager->>Storage: GET_DATA_REQUEST
Storage-->>Manager: GET_DATA_RESPONSE
end
Manager-->>Client: merged TensorDict
Client-->>App: TensorDict
end
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Areas needing focused review:
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
This PR implements a storage abstraction layer by refactoring the storage architecture:
TransferQueueStorageManager as an abstract base class for managing distributed storage unitsAsyncSimpleStorageManager as the default storage backendTransferQueueStorageSimpleUnit to SimpleStorageUnit with simplified responsibilitiesStorageMetaGroup) from metadata.py to storage/simple_backend.pyCopilot reviewed 24 out of 25 changed files in this pull request and generated 9 comments.
| File | Description |
|---|---|
| transfer_queue/utils/zmq_utils.py | Simplified imports and changed ZMQServerInfo from dataclass to regular class, modified request_id generation to use 8-char hex |
| transfer_queue/utils/utils.py | Added limit_pytorch_auto_parallel_threads context manager for PyTorch performance optimization |
| transfer_queue/storage/simple_backend.py | New file implementing StorageUnitData, SimpleStorageUnit, and StorageMetaGroup |
| transfer_queue/storage/managers/*.py | New storage manager abstraction with base class, factory, and async implementation |
| transfer_queue/metadata.py | Removed storage-related fields from SampleMeta and StorageMetaGroup class, simplified code |
| transfer_queue/controller.py | Removed storage mapping logic and multi-controller support, simplified handshake mechanism |
| transfer_queue/client.py | Refactored to use single controller and storage manager abstraction |
| tests/*.py | Updated tests to match new architecture |
| recipe/*.py | Updated demo scripts to use new API |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 13
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
transfer_queue/metadata.py (2)
211-229: Validate TensorDict/sample length before per‑sample add_fieldsPotential IndexError if len(self.samples) != len(fields). Validate and error early.
fields = _extract_field_metas(tensor_dict, set_all_ready) - if len(fields) > 0: - for idx, sample in enumerate(self.samples): - sample.add_fields(fields=fields[idx]) + if fields: + if len(self.samples) != len(fields): + raise ValueError(f"add_fields length mismatch: samples={len(self.samples)} vs fields={len(fields)}") + for idx, sample in enumerate(self.samples): + sample.add_fields(fields=fields[idx])
234-240: Fix type violation and inconsistent error handling in getitemThe current implementation has a type safety bug: when
self.samplesis empty,sample_metabecomes[], resulting insamples=[[]]which violates the type hintsamples: list[SampleMeta]. Additionally, error handling is inconsistent—the empty case lacks bounds checking while the non-empty case implicitly relies on list's__getitem__behavior.The proposed fix is correct. It adds explicit bounds checking with proper
IndexError, ensures type correctness by accessingself.samples[int(item)]directly (which is aSampleMetaobject), and provides consistent behavior for all cases.def __getitem__(self, item): - if isinstance(item, int | np.integer): - sample_meta = self.samples[item] if self.samples else [] - return BatchMeta(samples=[sample_meta], extra_info=self.extra_info) + if isinstance(item, (int, np.integer)): + if not (0 <= int(item) < len(self.samples)): + raise IndexError("BatchMeta index out of range") + return BatchMeta(samples=[self.samples[int(item)]], extra_info=self.extra_info) else: raise TypeError(f"Indexing with {type(item)} is not supported now!")transfer_queue/utils/zmq_utils.py (1)
116-131: Unsafe pickle deserialization on network data remains a critical security riskRemote code execution via malicious pickle is possible when an attacker gains access to the communication channel. The codebase deserializes untrusted ZMQ network data in 18+ locations without any validation or safe codec. Python's pickle documentation explicitly warns: "The pickle module is not secure. Only unpickle data you trust."
Replace pickle-based serialization with safer alternatives such as JSON, YAML, or MessagePack. The codebase already demonstrates msgpack usage in
serial_utils.pyfor data serialization—extend this pattern to ZMQMessage. Additionally, libzmq provides four levels of security including authenticated NULL, PLAIN, CURVE, and GSSAPI mechanisms that should be enforced on all sockets accepting external connections.Affected files:
transfer_queue/utils/zmq_utils.py(lines 116-131): Replacepickle.dumps/loadswith msgpack or JSON with explicit schema validation- 18+ calling sites receiving ZMQ data (identified in script output): Verify all depend on the fixed serialize/deserialize methods
transfer_queue/controller.py (2)
601-674: Handle unknown request types; avoid unbound response_msg.If an unexpected request_type arrives, response_msg is undefined and the thread crashes.
@@ def _process_request(self): - if request_msg.request_type == ZMQRequestType.GET_META: + if request_msg.request_type == ZMQRequestType.GET_META: @@ - elif request_msg.request_type == ZMQRequestType.CHECK_CONSUMPTION: + elif request_msg.request_type == ZMQRequestType.CHECK_CONSUMPTION: @@ - self.request_handle_socket.send_multipart([identity, response_msg.serialize()]) + else: + response_msg = ZMQMessage.create( + request_type=ZMQRequestType.PUT_GET_OPERATION_ERROR, + sender_id=self.controller_id, + receiver_id=request_msg.sender_id, + body={"message": f"Unknown request type: {request_msg.request_type}"}, + ) + self.request_handle_socket.send_multipart([identity, response_msg.serialize()])
681-728: Protect _update_data_status loop; prevent thread crash.A malformed message or deserialize error will crash the thread. Wrap recv/handle in try/except and continue.
@@ def _update_data_status(self): - while True: - logger.debug("Preparing _update_data_status...") - identity, serialized_msg = self.data_status_update_socket.recv_multipart() - logger.debug("Controller received update_data_status request!") - request_msg = ZMQMessage.deserialize(serialized_msg) + while True: + try: + logger.debug("Preparing _update_data_status...") + identity, serialized_msg = self.data_status_update_socket.recv_multipart() + logger.debug("Controller received update_data_status request!") + request_msg = ZMQMessage.deserialize(serialized_msg) @@ - elif request_msg.request_type == ZMQRequestType.NOTIFY_DATA_UPDATE_ERROR: + elif request_msg.request_type == ZMQRequestType.NOTIFY_DATA_UPDATE_ERROR: @@ - self.data_status_update_socket.send_multipart([identity, response_msg.serialize()]) + self.data_status_update_socket.send_multipart([identity, response_msg.serialize()]) + else: + logger.warning(f"Unexpected update request type: {getattr(request_msg,'request_type',None)}") + except Exception as e: + logger.error(f"_update_data_status error: {e}") + continuerecipe/simple_use_case/async_demo.py (2)
66-73: Fix NonTensorData stacking (runtime error).torch.stack on NonTensorData is invalid. Use NonTensorStack for non-tensor fields.
- output = TensorDict( - { - "generate_sequences_ids": output, - "non_tensor_data": torch.stack([NonTensorData("test_str") for _ in range(output.size(0))]), - "nested_tensor": torch.nested.as_nested_tensor([torch.randn(1, 2) for _ in range(output.size(0))]), - }, - batch_size=output.size(0), - ) + output = TensorDict( + { + "generate_sequences_ids": output, + "non_tensor_data": NonTensorStack(*["test_str" for _ in range(output.size(0))]), + "nested_tensor": torch.nested.as_nested_tensor([torch.randn(1, 2) for _ in range(output.size(0))]), + }, + batch_size=output.size(0), + )
118-125: Same fix for async server generate().- output = TensorDict( - { - "generate_sequences_ids": data, - "non_tensor_data": torch.stack([NonTensorData("test_str") for _ in range(data.size(0))]), - "nested_tensor": torch.nested.as_nested_tensor([torch.randn(1, 2) for _ in range(data.size(0))]), - }, - batch_size=data.size(0), - ) + output = TensorDict( + { + "generate_sequences_ids": data, + "non_tensor_data": NonTensorStack(*["test_str" for _ in range(data.size(0))]), + "nested_tensor": torch.nested.as_nested_tensor([torch.randn(1, 2) for _ in range(data.size(0))]), + }, + batch_size=data.size(0), + )tests/test_client.py (1)
164-171: Use enum for ZMQServerInfo.role for consistency.One instance still uses string; switch to TransferQueueRole.STORAGE.
- self.zmq_server_info = ZMQServerInfo( - role="TransferQueueStorage", + self.zmq_server_info = ZMQServerInfo( + role=TransferQueueRole.STORAGE, id=storage_id, ip="127.0.0.1", ports={ "put_get_socket": self.data_port, }, )recipe/simple_use_case/sync_demo.py (1)
104-111: Fix NonTensorData stacking in sync path.Use NonTensorStack for non-tensor fields.
- output = TensorDict( - { - "generate_sequences_ids": output, - "non_tensor_data": torch.stack([NonTensorData("test_str") for _ in range(output.size(0))]), - "nested_tensor": torch.nested.as_nested_tensor([torch.randn(1, 2) for _ in range(output.size(0))]), - }, - batch_size=output.size(0), - ) + output = TensorDict( + { + "generate_sequences_ids": output, + "non_tensor_data": NonTensorStack(*["test_str" for _ in range(output.size(0))]), + "nested_tensor": torch.nested.as_nested_tensor([torch.randn(1, 2) for _ in range(output.size(0))]), + }, + batch_size=output.size(0), + )transfer_queue/client.py (2)
134-142: Remove redundant socket close call.The socket cleanup contains duplicate
close()calls. Line 137 already closes the socket if not closed, and line 138 attempts to close it again unconditionally, which will operate on an already-closed socket.Apply this diff to fix the redundant close:
try: if not sock.closed: sock.setsockopt(zmq.LINGER, -1) sock.close() - sock.close(linger=0) except Exception as e: logger.warning(f"[{self.client_id}]: Error closing socket to Controller {server_info.id}: {e}")
236-307: Add existence check for storage_manager.The method delegates to
self.storage_manager.put_data()(line 303) without verifying thatstorage_managerwas initialized. If a user forgets to callinitialize_storage_manager(), this will raise anAttributeError.Add a validation check at the beginning of the method:
async def async_put( self, data: TensorDict, metadata: Optional[BatchMeta] = None, global_step: Optional[int] = None, ): """Asynchronously write data to storage units based on metadata. If metadata is not provided, it will be created automatically using insert mode with the provided data fields and global_step. Args: data: Data to write as TensorDict metadata: Records the metadata of a batch of data samples, containing index and storage unit information. If None, metadata will be auto-generated. global_step: Current processing step (required if metadata is not provided) Raises: ValueError: If metadata is None or empty, or if global_step is None when metadata is not provided RuntimeError: If storage operation fails Example: >>> batch_size = 4 >>> seq_len = 16 >>> current_step = 0 >>> # Example 1: Normal usage with existing metadata >>> batch_meta = asyncio.run(client.async_get_meta( ... data_fields=["prompts", "attention_mask"], ... batch_size=batch_size, ... global_step=current_step, ... mode="fetch", ... get_n_samples=False, ... task_name="generate_sequences", ... )) >>> batch = asyncio.run(client.async_get_data(batch_meta)) >>> output = TensorDict({"response": torch.randn(batch_size, seq_len)}) >>> asyncio.run(client.async_put(data=output, metadata=batch_meta)) >>> >>> # Example 2: Initial data insertion without pre-existing metadata >>> # BE CAREFUL: this usage may overwrite any unconsumed data in the given global_step! >>> # Please make sure the corresponding global_step is empty before calling the async_put() >>> # without metadata. >>> # Now we only support put all the data of the corresponding global step in once. You should repeat with >>> # interleave the initial data if n_sample > 1 before calling the async_put(). >>> original_prompts = torch.randn(batch_size, seq_len) >>> n_samples = 4 >>> prompts_repeated = torch.repeat_interleave(original_prompts, n_samples, dim=0) >>> prompts_repeated_batch = TensorDict({"prompts": prompts_repeated}) >>> # This will create metadata in "insert" mode internally. >>> asyncio.run(client.async_put(data=prompts_repeated_batch, global_step=current_step)) """ + if not hasattr(self, "storage_manager") or self.storage_manager is None: + raise RuntimeError( + f"[{self.client_id}]: Storage manager not initialized. " + "Call initialize_storage_manager() before performing storage operations." + ) + if metadata is None: assert global_step is not None, "global_steps must be provided if metadata is not given"
transfer_queue/utils/utils.py (1)
106-106: Consider expanding the docstring.The docstring is quite brief and doesn't describe parameters, return behavior, or usage examples. Consider adding more detail about when and why this should be used.
Example enhancement:
- """Prevent PyTorch from overdoing the automatic parallelism during torch.stack() operation""" + """ + Temporarily limit PyTorch's thread count to prevent excessive parallelism. + + Useful during operations like torch.stack() where PyTorch's automatic + parallelism may spawn too many threads on high-core-count machines. + + Args: + target_num_threads: Maximum threads to use. If None, auto-determined + based on physical cores (16 if ≥16 cores, else physical core count). + + Yields: + None + + Raises: + RuntimeError: If target_num_threads exceeds logical CPU cores. + """transfer_queue/storage/clients/factory.py (1)
1-13: Placeholder module: add a minimal stub or remove until usedThis file only contains a license header. Either remove it for now or add a TODO and a minimal Factory interface to avoid confusion during discovery.
transfer_queue/utils/zmq_utils.py (4)
78-85: Ensure JSON-serializable role in to_dictrole is an enum; return role.value (or str(role)) to make this dict safe for JSON/log serialization.
- return { - "role": self.role, + return { + "role": self.role.value, "id": self.id, "ip": self.ip, "ports": self.ports, }
86-88: Typo: class name in strThe string says ZMQSocketInfo; class is ZMQServerInfo. Fix for clarity.
- return f"ZMQSocketInfo(role={self.role}, id={self.id}, ip={self.ip}, ports={self.ports})" + return f"ZMQServerInfo(role={self.role}, id={self.id}, ip={self.ip}, ports={self.ports})"
112-114: Short request_id (8 hex) increases collision risk under load8 hex chars (~32 bits) can collide in busy systems. Prefer 16–32 hex for safety.
- request_id=str(uuid4().hex[:8]), + request_id=uuid4().hex[:16], # or omit slicing
69-76: Align get_free_port return type with ports typingget_free_port returns an int but is annotated as str; fix typing and minor naming nits.
-def get_free_port() -> str: - with socket.socket() as sock: - sock.bind(("", 0)) - return sock.getsockname()[1] +def get_free_port() -> int: + with socket.socket() as s: + s.bind(("", 0)) + return int(s.getsockname()[1])Also ensure ZMQServerInfo.ports uses dict[str, int] (see prior comment).
Also applies to: 134-137
transfer_queue/metadata.py (4)
98-123: union() returns self but doc promises a new object/NoneDoc says “New SampleMeta (None if validation fails)”; implementation mutates and returns self, and raises on validation failure. Align doc with behavior.
- Returns: - New SampleMeta with unioned fields (None if validation fails) + Returns: + SampleMeta (self) with unioned fields. Raises ValueError on validation failure.
153-160: field_names cache may not represent “all unique field names”You cache field names from the first sample but the property doc says “all unique field names”. Either compute the union across samples or update the doc.
Possible fix:
- object.__setattr__(self, "_field_names", sorted(self.samples[0].field_names)) + union = sorted({name for s in self.samples for name in s.field_names}) + object.__setattr__(self, "_field_names", union)Also applies to: 171-175
295-299: extra_info merge semantics differ from BatchMeta.unionconcat uses ChainMap (first-wins), union uses dict unpacking (last-wins). Pick one policy for consistency; recommend last-wins.
- merged_extra_info = dict(ChainMap(*(chunk.extra_info for chunk in data))) + merged_extra_info = {} + for chunk in data: + merged_extra_info |= chunk.extra_info # last-wins
441-455: Prefer len(tensor_dict) for batch size; minor claritybatch_size = tensor_dict.batch_size[0] works but len(tensor_dict) is clearer and resilient.
- batch_size = tensor_dict.batch_size[0] + batch_size = len(tensor_dict).github/workflows/python-package.yml (1)
30-31: Use canonical package name pytest-asyncioThe canonical PyPI project name is "pytest-asyncio" (dash), even though pip normalizes both forms. Switch to the official name for clarity.
- python -m pip install flake8 pytest build pytest_asyncio + python -m pip install flake8 pytest build pytest-asynciotransfer_queue/storage/managers/__init__.py (1)
15-17: Prefer lazy re‑exports to reduce import-time cost and cycle riskReplace eager imports with on-demand attribute loading via getattr.
Apply:
+from importlib import import_module + -from .base import TransferQueueStorageManager -from .factory import TransferQueueStorageManagerFactory -from .simple_backend_manager import AsyncSimpleStorageManager +_EXPORTS = { + "TransferQueueStorageManager": (".base", "TransferQueueStorageManager"), + "TransferQueueStorageManagerFactory": (".factory", "TransferQueueStorageManagerFactory"), + "AsyncSimpleStorageManager": (".simple_backend_manager", "AsyncSimpleStorageManager"), +} + +def __getattr__(name: str): + if name in _EXPORTS: + mod_path, attr = _EXPORTS[name] + return getattr(import_module(mod_path, __package__), attr) + raise AttributeError(name)transfer_queue/storage/__init__.py (1)
15-17: Consider lazy re‑exports for the storage package, mirroring managers/init.pyHelps avoid importing heavy deps at module import time.
README.md (4)
55-56: Align terminology with code (“StorageManager” + “SimpleStorageUnit”)Replace “written to the TransferQueueStorage” with “written to the storage backend (e.g., SimpleStorageUnit via TransferQueueStorageManager)” for consistency.
24-25: Add alt text to images for accessibilityProvide descriptive alt attributes for all tags.
Example:
-<img src=".../1761356010763-....png" width="70%"> +<img src=".../1761356010763-....png" width="70%" alt="TransferQueue architecture overview">Also applies to: 32-33, 61-62, 125-126, 144-146, 178-179
152-174: Document test extras in Quick StartSince pyproject defines [project.optional-dependencies].test, add:
- pip install -e ".[test]"
- pytest -q
10-13: Review third‑party embedThe DeepWiki badge references devin.ai assets; consider replacing with a static badge or project-hosted asset to avoid external tracking/fragility.
pyproject.toml (3)
83-86: Set pytest asyncio mode to avoid deprecation warningsAdd asyncio_mode = "auto".
[tool.pytest.ini_options] filterwarnings = [ "ignore:.*PyTorch API of nested tensors.*prototype.*:UserWarning", ] +asyncio_mode = "auto"
71-79: Avoid blanket mypy ignore_errorsPrefer targeted silences; set ignore_errors = false and use per-module overrides.
-[tool.mypy] +[tool.mypy] pretty = true ignore_missing_imports = true explicit_package_bases = true follow_imports = "skip" - -# Blanket silence -ignore_errors = true +ignore_errors = false
110-114: Expose test extras in docs and CILeverage the new test extra: pip install -e ".[test]".
tests/test_async_simple_storage_manager.py (6)
25-33: Avoid sys.path hacks in testsRely on editable install or pytest config. Suggest pip install -e ".[test]" and remove manual path injection.
-parent_dir = Path(__file__).resolve().parent.parent -sys.path.append(str(parent_dir)) +# Prefer: install package in editable mode for tests or configure pytest to run from repo root.
35-89: Prefer constructing the class normally vs newUsing new risks partial initialization. Patch handshake like in other tests and call AsyncSimpleStorageManager(config).
198-227: Add resource cleanup to avoid lingering ZMQ contextsCall manager.close() in a finally or use addfinalizer.
- manager = AsyncSimpleStorageManager(config) + manager = AsyncSimpleStorageManager(config) + try: + # assertions... + assert manager.global_index_storage_unit_mapping(0) == "storage_0" + # ... + finally: + manager.close()
246-301: Also close manager in error-handling testAvoid leaking sockets/contexts across tests.
- manager = AsyncSimpleStorageManager(config) + manager = AsyncSimpleStorageManager(config) + try: + # mocked operations and assertions... + pass + finally: + manager.close()
147-150: Strengthen assertionsAssert notify_data_update arguments and that _put/_get/_clear were called with expected shard targets.
Example:
manager.notify_data_update.assert_awaited_once() manager._put_to_single_storage_unit.assert_awaited() calls = [c.kwargs.get("target_storage_unit") for c in manager._put_to_single_storage_unit.await_args_list] assert set(calls) == {"storage_0", "storage_1"}Also applies to: 151-157
1-223: Sanity-check: pickle over ZMQZMQMessage uses pickle serialization; ensure tests cover round-trip for nontrivial payloads and consider documenting the security trade-offs.
tests/test_controller.py (3)
26-27: Avoid importing internals in tests unless necessaryImporting TQ_INIT_FIELD_NUM from transfer_queue.controller couples tests to internals. Prefer asserting behaviors via public APIs or expose a stable constant through the package root if needed.
If TQ_INIT_FIELD_NUM must be asserted, consider re-exporting it in transfer_queue/init.py.
97-129: Add invariant checks to strengthen metadata testThe reorder/global_indexes assertions look right. Add simple invariants to catch regressions (size and field_names unchanged by reorder).
metadata = ray.get( tq_controller._get_metadata.remote( data_fields=data_fields, batch_size=global_batch_size * n_samples, global_step=global_step, mode="insert", ) ) + # Invariants + assert metadata.size == global_batch_size * n_samples + assert metadata.field_names == sorted(data_fields) metadata.reorder([15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0])
65-84: Tests rely on private controller methods—consider adding public accessors or test helper methodsBoth
_update_production_status(line 78) and_get_consumption_status(line 93) are internal methods that lack public alternatives. While the.remote()calls are valid (TransferQueueController is properly decorated with@ray.remote), testing through private methods risks breakage during future refactors. Either add public wrapper methods for testing, create dedicated test helper methods in the controller, or add comments explaining why private access is necessary.transfer_queue/storage/managers/factory.py (1)
25-41: Harden factory: normalize keys, guard duplicates, and default to class nameMake manager_type case-insensitive, prevent accidental overwrite, and use the class name by default during registration.
@@ - def register(cls, manager_type: str, manager_cls: type[TransferQueueStorageManager]): + def register(cls, manager_type: str | None, manager_cls: type[TransferQueueStorageManager]): if not issubclass(manager_cls, TransferQueueStorageManager): raise TypeError( f"manager_cls {getattr(manager_cls, '__name__', repr(manager_cls))} must be " f"a subclass of TransferQueueStorageManager" ) - cls._registry[manager_type] = manager_cls + key = (manager_type or manager_cls.__name__).lower() + if key in cls._registry and cls._registry[key] is not manager_cls: + raise ValueError(f"Duplicate registration for manager_type '{key}'") + cls._registry[key] = manager_cls @@ - def create(cls, manager_type: str, config: dict[str, Any]) -> TransferQueueStorageManager: - if manager_type not in cls._registry: + def create(cls, manager_type: str, config: dict[str, Any]) -> TransferQueueStorageManager: + key = manager_type.lower() + if key not in cls._registry: raise ValueError( - f"Unknown manager_type: {manager_type}. Supported managers include: {list(cls._registry.keys())}" + f"Unknown manager_type: {manager_type}. Supported: {list(cls._registry.keys())}" ) - return cls._registry[manager_type](config) + return cls._registry[key](config) @@ -TransferQueueStorageManagerFactory.register("AsyncSimpleStorageManager", AsyncSimpleStorageManager) +TransferQueueStorageManagerFactory.register(None, AsyncSimpleStorageManager)Also applies to: 43-44
tests/test_simple_storage_unit.py (3)
34-51: Set SNDTIMEO and client identity for stability and debuggabilityAvoid indefinite send on backpressure and add an identity to ease ROUTER-side tracing.
class MockStorageClient: @@ - def __init__(self, storage_put_get_address): + def __init__(self, storage_put_get_address, identity: str | None = None): self.context = zmq.Context() - self.socket = self.context.socket(zmq.DEALER) - self.socket.setsockopt(zmq.RCVTIMEO, 5000) # 5 second timeout + self.socket = self.context.socket(zmq.DEALER) + if identity: + self.socket.setsockopt(zmq.IDENTITY, identity.encode()) + self.socket.setsockopt(zmq.RCVTIMEO, 5000) # 5s + self.socket.setsockopt(zmq.SNDTIMEO, 5000) # 5s self.socket.connect(storage_put_get_address)
92-99: Replace fixed sleep with readiness probe to reduce flakinessActively probe the ROUTER by issuing a harmless CLEAR with empty indexes; retry a few times instead of sleeping.
- put_get_address = zmq_info.to_addr("put_get_socket") - time.sleep(1) # Wait for socket to be ready + put_get_address = zmq_info.to_addr("put_get_socket") + # Probe readiness: send a no-op CLEAR ([]) with short timeout + ready = False + for attempt in range(10): + try: + client = MockStorageClient(put_get_address, identity=f"probe_{attempt}") + resp = client.send_clear("probe", []) + # Accept either CLEAR_DATA_RESPONSE or CLEAR_DATA_ERROR (server up) + if resp.request_type in {ZMQRequestType.CLEAR_DATA_RESPONSE, ZMQRequestType.CLEAR_DATA_ERROR}: + ready = True + client.close() + break + except Exception: + time.sleep(0.1) + assert ready, "StorageUnit put_get_socket not ready in time"
202-256: Gate performance test to avoid CI flakinessMark as slow and allow opt-in via env var to keep PR signal clean.
-def test_performance_basic(storage_setup): +@pytest.mark.slow +def test_performance_basic(storage_setup): @@ - assert avg_put_latency < 1500, f"Avg PUT latency {avg_put_latency}ms exceeds threshold" - assert avg_get_latency < 1500, f"Avg GET latency {avg_get_latency}ms exceeds threshold" + if os.getenv("TQ_RUN_PERF_TESTS", "0") == "1": + assert avg_put_latency < 1500, f"Avg PUT latency {avg_put_latency}ms exceeds threshold" + assert avg_get_latency < 1500, f"Avg GET latency {avg_get_latency}ms exceeds threshold"transfer_queue/storage/managers/base.py (2)
65-76: Set DEALER socket timeouts to avoid accidental indefinite blocksAdd conservative SNDTIMEO/RCVTIMEO on controller_handshake_socket and data_status_update_socket.
self.controller_handshake_socket = create_zmq_socket( self.zmq_context, zmq.DEALER, identity=f"{self.storage_manager_id}-controller_handshake_socket-{uuid4().hex[:8]}".encode(), ) + self.controller_handshake_socket.setsockopt(zmq.RCVTIMEO, 10_000) + self.controller_handshake_socket.setsockopt(zmq.SNDTIMEO, 10_000) self.data_status_update_socket = create_zmq_socket( self.zmq_context, zmq.DEALER, identity=f"{self.storage_manager_id}-data_status_update_socket-{uuid4().hex[:8]}".encode(), ) + self.data_status_update_socket.setsockopt(zmq.RCVTIMEO, 10_000) + self.data_status_update_socket.setsockopt(zmq.SNDTIMEO, 10_000) self.data_status_update_socket.connect(self.controller_info.to_addr("data_status_update_socket"))
218-230: Don’t wait for ACK after emitting NOTIFY_DATA_UPDATE_ERRORIf sending the update fails, you emit an error message and still wait for an ACK, which will likely never arrive. Return early.
except Exception as e: request_msg = ZMQMessage.create( request_type=ZMQRequestType.NOTIFY_DATA_UPDATE_ERROR, sender_id=self.storage_manager_id, body={ "message": f"Failed to notify data status update information from " f"storage manager id #{self.storage_manager_id}, " f"detail error message: {str(e)}" }, ).serialize() self.data_status_update_socket.send(request_msg) + return # Avoid waiting for ACK after error # Make sure controller successfully receives data status update information.Also applies to: 252-257
transfer_queue/storage/managers/simple_backend_manager.py (1)
322-325: Reduce log verbosity for GET responsesLogging entire response bodies at info can be noisy and expensive; use debug or log minimal identifiers.
- logger.info( - f"[{self.storage_manager_id}]: get data response from storage unit " - f"{target_storage_unit}: {response_msg}" - ) + logger.debug( + "[%s]: GET response from %s (type=%s, size=%s)", + self.storage_manager_id, + target_storage_unit, + getattr(response_msg, "request_type", None), + len(getattr(response_msg.body, "get", lambda *_: [])("data", {})) if hasattr(response_msg, "body") else None, + )transfer_queue/storage/simple_backend.py (3)
178-185: Add graceful shutdown; set socket LINGER and close Context.Provide close() to avoid resource leaks and hanging ROUTER sockets on actor teardown.
@@ class SimpleStorageUnit: def _init_zmq_socket(self) -> None: @@ self.put_get_socket = create_zmq_socket(self.zmq_context, zmq.ROUTER) self.put_get_socket.bind(self.zmq_server_info.to_addr("put_get_socket")) + + def close(self) -> None: + try: + if hasattr(self, "put_get_socket") and not self.put_get_socket.closed: + self.put_get_socket.setsockopt(zmq.LINGER, 0) + self.put_get_socket.close() + finally: + if hasattr(self, "zmq_context"): + self.zmq_context.term() @@ def get_zmq_server_info(self) -> ZMQServerInfo:Also applies to: 195-197, 345-352
178-183: Port type inconsistency.ZMQServerInfo.ports is typed dict[str, str], but get_free_port returns int; you pass ints. Either cast to str here or update the annotation to int.
- ports={"put_get_socket": get_free_port()}, + ports={"put_get_socket": str(get_free_port())},
382-389: Deterministic field order for reproducibility.get_field_names returns an arbitrary set order. Sort to stabilize downstream behavior and tests.
- return list(all_fields) + return sorted(all_fields)transfer_queue/controller.py (1)
512-521: Ports typing consistency.As with storage, cast port ints to str or change ZMQServerInfo annotation.
- "handshake_socket": self._handshake_socket_port, - "request_handle_socket": self._request_handle_socket_port, - "data_status_update_socket": self._data_status_update_socket_port, + "handshake_socket": str(self._handshake_socket_port), + "request_handle_socket": str(self._request_handle_socket_port), + "data_status_update_socket": str(self._data_status_update_socket_port),
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
.github/workflows/python-package.yml (1 hunks)README.md (3 hunks)pyproject.toml (3 hunks)recipe/simple_use_case/async_demo.py (7 hunks)recipe/simple_use_case/sync_demo.py (5 hunks)requirements.txt (1 hunks)tests/test_async_simple_storage_manager.py (1 hunks)tests/test_client.py (10 hunks)tests/test_controller.py (2 hunks)tests/test_simple_storage_unit.py (12 hunks)transfer_queue/__init__.py (2 hunks)transfer_queue/client.py (11 hunks)transfer_queue/controller.py (6 hunks)transfer_queue/metadata.py (7 hunks)transfer_queue/storage.py (0 hunks)transfer_queue/storage/__init__.py (1 hunks)transfer_queue/storage/clients/__init__.py (1 hunks)transfer_queue/storage/clients/factory.py (1 hunks)transfer_queue/storage/managers/__init__.py (1 hunks)transfer_queue/storage/managers/base.py (1 hunks)transfer_queue/storage/managers/factory.py (1 hunks)transfer_queue/storage/managers/simple_backend_manager.py (1 hunks)transfer_queue/storage/simple_backend.py (1 hunks)transfer_queue/utils/utils.py (2 hunks)transfer_queue/utils/zmq_utils.py (3 hunks)transfer_queue/storage/managers/base.py (1)
TransferQueueStorageManager(40-268)transfer_queue/storage/managers/factory.py (1)
TransferQueueStorageManagerFactory(20-40)transfer_queue/storage/managers/simple_backend_manager.py (1)
AsyncSimpleStorageManager(37-415)
transfer_queue/controller.py (1)
TransferQueueController(55-750)
transfer_queue/metadata.py (3)
BatchMeta(136-417)FieldMeta(28-45)SampleMeta(49-132)transfer_queue/storage/managers/simple_backend_manager.py (7)
AsyncSimpleStorageManager(37-415)_put_to_single_storage_unit(205-241)_get_from_single_storage_unit(306-337)_clear_single_storage_unit(366-388)put_data(163-202)get_data(243-303)clear_data(339-363)transfer_queue/utils/utils.py (1)
TransferQueueRole(36-39)transfer_queue/utils/zmq_utils.py (5)
ZMQMessage(91-131)ZMQRequestType(31-65)ZMQServerInfo(68-87)create(100-114)serialize(116-118)transfer_queue/storage/managers/base.py (5)
_connect_to_controller(55-82)notify_data_update(172-256)put_data(259-260)get_data(263-264)clear_data(267-268)
transfer_queue/storage/managers/simple_backend_manager.py (1)
AsyncSimpleStorageManager(37-415)transfer_queue/storage/managers/base.py (1)
TransferQueueStorageManager(40-268)transfer_queue/storage/managers/factory.py (1)
TransferQueueStorageManagerFactory(20-40)transfer_queue/storage/simple_backend.py (3)
SimpleStorageUnit(150-351)StorageMetaGroup(355-449)StorageUnitData(41-146)
transfer_queue/metadata.py (2)
BatchMeta(136-417)global_indexes(167-169)transfer_queue/utils/zmq_utils.py (8)
ZMQMessage(91-131)ZMQRequestType(31-65)ZMQServerInfo(68-87)create_zmq_socket(140-170)to_addr(75-76)deserialize(121-131)create(100-114)serialize(116-118)transfer_queue/storage/managers/factory.py (2)
register(26-32)create(35-40)transfer_queue/storage/managers/simple_backend_manager.py (3)
put_data(163-202)get_data(243-303)clear_data(339-363)transfer_queue/storage/simple_backend.py (2)
put_data(105-126)get_data(66-103)
transfer_queue/storage/managers/base.py (1)
TransferQueueStorageManager(40-268)transfer_queue/storage/managers/simple_backend_manager.py (1)
AsyncSimpleStorageManager(37-415)transfer_queue/utils/zmq_utils.py (1)
create(100-114)
transfer_queue/metadata.py (2)
SampleMeta(49-132)batch_index(70-72)transfer_queue/utils/utils.py (1)
TransferQueueRole(36-39)transfer_queue/utils/zmq_utils.py (9)
ZMQMessage(91-131)ZMQRequestType(31-65)ZMQServerInfo(68-87)create_zmq_socket(140-170)get_free_port(134-137)to_addr(75-76)deserialize(121-131)create(100-114)serialize(116-118)
transfer_queue/metadata.py (3)
BatchMeta(136-417)global_indexes(167-169)batch_index(70-72)transfer_queue/storage/managers/base.py (5)
TransferQueueStorageManager(40-268)put_data(259-260)notify_data_update(172-256)get_data(263-264)clear_data(267-268)transfer_queue/storage/simple_backend.py (5)
StorageMetaGroup(355-449)put_data(105-126)get_transfer_data(389-428)get_data(66-103)add_sample_meta(365-368)transfer_queue/utils/utils.py (1)
limit_pytorch_auto_parallel_threads(105-132)transfer_queue/utils/zmq_utils.py (7)
ZMQMessage(91-131)ZMQRequestType(31-65)ZMQServerInfo(68-87)create_zmq_socket(140-170)create(100-114)serialize(116-118)deserialize(121-131)
transfer_queue/utils/utils.py (1)
TransferQueueRole(36-39)
transfer_queue/storage/simple_backend.py (2)
SimpleStorageUnit(150-351)StorageUnitData(41-146)transfer_queue/utils/zmq_utils.py (6)
ZMQMessage(91-131)ZMQRequestType(31-65)create(100-114)serialize(116-118)deserialize(121-131)to_addr(75-76)
transfer_queue/storage/simple_backend.py (1)
SimpleStorageUnit(150-351)
transfer_queue/utils/utils.py (1)
TransferQueueRole(36-39)transfer_queue/utils/zmq_utils.py (1)
ZMQServerInfo(68-87)transfer_queue/client.py (3)
initialize_storage_manager(73-89)get_data(527-536)TransferQueueClient(465-544)transfer_queue/storage/managers/base.py (3)
put_data(259-260)get_data(263-264)clear_data(267-268)
transfer_queue/utils/zmq_utils.py (6)
ZMQServerInfo(68-87)ZMQMessage(91-131)deserialize(121-131)ZMQRequestType(31-65)create(100-114)serialize(116-118)
transfer_queue/controller.py (2)
clear(737-750)get_zmq_server_info(729-735)transfer_queue/metadata.py (2)
BatchMeta(136-417)size(162-164)transfer_queue/storage/simple_backend.py (5)
put_data(105-126)size(431-433)get_data(66-103)clear(128-146)get_zmq_server_info(345-351)transfer_queue/storage/managers/base.py (4)
TransferQueueStorageManager(40-268)put_data(259-260)get_data(263-264)clear_data(267-268)transfer_queue/storage/managers/factory.py (2)
TransferQueueStorageManagerFactory(20-40)create(35-40)transfer_queue/utils/zmq_utils.py (5)
ZMQServerInfo(68-87)create(100-114)create_zmq_socket(140-170)ZMQMessage(91-131)ZMQRequestType(31-65)transfer_queue/storage/managers/simple_backend_manager.py (7)
decorator(110-159)wrapper(112-157)put_data(163-202)get_data(243-303)clear_data(339-363)close(398-415)get_zmq_server_info(390-396)
transfer_queue/storage/simple_backend.py (1)
SimpleStorageUnit(150-351)transfer_queue/client.py (4)
TransferQueueClient(465-544)process_zmq_server_info(547-577)initialize_storage_manager(73-89)close(455-462)transfer_queue/utils/utils.py (1)
get_placement_group(49-63)
transfer_queue/utils/utils.py (1)
ProductionStatus(43-46)
transfer_queue/storage/simple_backend.py (1)
SimpleStorageUnit(150-351)transfer_queue/client.py (5)
AsyncTransferQueueClient(47-462)initialize_storage_manager(73-89)process_zmq_server_info(547-577)async_clear(340-363)close(455-462)transfer_queue/utils/utils.py (1)
get_placement_group(49-63)
transfer_queue/storage/clients/__init__.py (1)
1-15: LGTM!This package initializer with the Apache 2.0 license header is appropriately structured for the new
transfer_queue/storage/clientssubpackage introduced as part of the storage architecture refactoring. The placeholder comment correctly indicates this is reserved for future client implementations.transfer_queue/utils/utils.py (1)
15-15: LGTM!The imports are appropriate for the new context manager functionality.
Also applies to: 17-17, 19-19
requirements.txt (1)
7-7: LGTM! The psutil dependency is secure.The security advisory check found a historical "Double Free in psutil" vulnerability (HIGH severity) affecting versions <= 5.6.5, which was patched in version 5.6.6 (March 2020). Since no version is pinned, pip will install the latest stable version of psutil, which is well beyond the vulnerable range and includes the security patch.
transfer_queue/__init__.py (1)
24-25: Storage package properly exports SimpleStorageUnit—verification passed.The import at transfer_queue/init.py:24 is correctly supported: transfer_queue/storage/init.py re-exports SimpleStorageUnit from simple_backend.py and includes it in all. Public API is stable.
transfer_queue/storage/managers/__init__.py (1)
19-23: Public surface looks goodall matches the intended API.
transfer_queue/storage/__init__.py (1)
18-25: API consolidation LGTMExports are coherent and discoverable.
README.md (1)
69-72: Good clarity on the new storage abstractionThe Data Plane section accurately reflects TransferQueueStorageManager and AsyncSimpleStorageManager.
Also applies to: 85-86
tests/test_async_simple_storage_manager.py (1)
1-223: Stale symbol verification passedConfirmed: No references to old symbol names (TransferQueueStorageSimpleUnit, TransferQueueStorageSimple, TQStorageSimple) found in the codebase. Rename was completed cleanly without broken imports.
pyproject.toml (1)
105-109: Files verified and correctly configuredBoth
transfer_queue/version/versionandrequirements.txtexist at the paths specified in pyproject.toml:
- Version file contains valid version string:
0.0.1.dev- Requirements file contains valid dependencies
- Both files are part of the package source tree and will be included in builds
The dynamic version and dependencies configuration is correct.
recipe/simple_use_case/sync_demo.py (1)
166-173: Same fix for compute_old_log_prob path.- output = TensorDict({"old_log_prob": output}, batch_size=output.size(0)) + output = TensorDict({"old_log_prob": output}, batch_size=output.size(0))Note: No change needed here; included to confirm NonTensorStack not required in this path. If other non-tensor fields are added later, prefer NonTensorStack.
transfer_queue/client.py (9)
25-35: LGTM!The import updates correctly reflect the new storage manager architecture, including
SimpleStorageUnit,TransferQueueStorageManager, andTransferQueueStorageManagerFactory.
54-71: LGTM!The constructor correctly implements the single-controller architecture with proper validation of the
controller_infoparameter.
73-89: LGTM!The storage manager initialization correctly uses the factory pattern. However, note that this method is optional, which means
self.storage_managermay not exist when storage operations are called (see related comments on Lines 303, 336, 359).
148-234: LGTM!The method correctly routes metadata requests to the single controller, and the comprehensive docstring with examples greatly improves usability.
365-431: LGTM!The private helper methods for clearing operations are well-structured and correctly use the
dynamic_socketdecorator for controller communication.
433-453: LGTM!The placeholder methods are appropriately marked with TODO comments and have the necessary decorator in place for future implementation.
455-462: LGTM!The
close()method provides proper resource cleanup with appropriate error handling to prevent resource leaks.
465-544: LGTM!The synchronous wrapper class correctly delegates to async methods and maintains consistency with the single-controller architecture. Docstrings are clear and helpful.
547-577: LGTM!The utility function correctly handles both single handler and dictionary cases, with appropriate type hints reflecting the expanded set of supported handler types.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
| def _wait_connection(self): | ||
| """Wait for all storage instances to complete handshake. | ||
| """ | ||
| Wait for storage instances to complete handshake with retransmission support. | ||
|
|
||
| This method runs continuously in a dedicated thread to handle handshake responses for | ||
| new TransferQueueStorageManager connections. It listens for handshake requests from | ||
| storage managers and responds with acknowledgments, ensuring that new storage units can | ||
| connect and be tracked even after the initial startup phase. | ||
|
|
||
| Clients don't need handshake to support dynamic scaling. Continuously | ||
| listens for handshake messages until all expected storage units connect. | ||
| The method includes a retransmission mechanism - if a storage manager resends a | ||
| HANDSHAKE message (e.g., due to network issues or timeout), the controller will | ||
| detect it as a duplicate and resend the ACK response without treating it as a new | ||
| connection. | ||
| """ | ||
| # TODO(zjj): Consider if retransmission is needed (assuming cases where Storage doesn't receive ACK) | ||
| connected_storage_units = set() | ||
| while len(connected_storage_units) < self.num_storage_units: | ||
| identity, serialized_msg = self.handshake_socket.recv_multipart() | ||
| request_msg = ZMQMessage.deserialize(serialized_msg) | ||
| if request_msg.request_type == ZMQRequestType.HANDSHAKE: | ||
| connected_storage_units.add(request_msg.sender_id) | ||
| response_msg = ZMQMessage.create( | ||
| request_type=ZMQRequestType.HANDSHAKE_ACK, | ||
| sender_id=self.controller_id, | ||
| body={}, | ||
| ).serialize() | ||
| self.handshake_socket.send_multipart([identity, response_msg]) | ||
| logger.info("Controller sent handshake ack successfully!") | ||
| self.global_index_storage_id_mapping = np.array(sorted(list(connected_storage_units)))[ | ||
| self._global_index_storage_rank_mapping | ||
| ] | ||
| self.handshake_done.set() | ||
| poller = zmq.Poller() | ||
| poller.register(self.handshake_socket, zmq.POLLIN) | ||
|
|
||
| logger.info(f"Controller {self.controller_id} started waiting for storage connections...") | ||
|
|
||
| while True: | ||
| # Wait for messages with timeout | ||
| socks = dict(poller.poll(TQ_CONTROLLER_CONNECTION_CHECK_INTERVAL * 1000)) | ||
|
|
||
| if self.handshake_socket in socks: | ||
| try: | ||
| identity, serialized_msg = self.handshake_socket.recv_multipart() | ||
| request_msg = ZMQMessage.deserialize(serialized_msg) | ||
|
|
||
| if request_msg.request_type == ZMQRequestType.HANDSHAKE: | ||
| storage_manager_id = request_msg.sender_id | ||
|
|
||
| # Always send ACK for HANDSHAKE (handles retransmission) | ||
| response_msg = ZMQMessage.create( | ||
| request_type=ZMQRequestType.HANDSHAKE_ACK, | ||
| sender_id=self.controller_id, | ||
| body={}, | ||
| ).serialize() | ||
| self.handshake_socket.send_multipart([identity, response_msg]) | ||
|
|
||
| # Track new connections | ||
| if storage_manager_id not in self._connected_storage_managers: | ||
| self._connected_storage_managers.add(storage_manager_id) | ||
| storage_manager_type = request_msg.body.get("storage_manager_type", "Unknown") | ||
| logger.info( | ||
| f"Controller {self.controller_id} received handshake from " | ||
| f"storage manager {storage_manager_id} (type: {storage_manager_type}). " | ||
| f"Total connected: {len(self._connected_storage_managers)}" | ||
| ) | ||
| else: | ||
| logger.debug( | ||
| f"Controller {self.controller_id} received duplicate handshake from " | ||
| f"storage manager {storage_manager_id}. Resending ACK." | ||
| ) | ||
|
|
||
| except Exception as e: | ||
| logger.error(f"Controller {self.controller_id} error processing handshake: {e}") | ||
|
|
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Harden handshake against spoofing and pickle risks.
Handshake path accepts arbitrary pickled payloads over network. Add message authentication (e.g., HMAC), consider CURVE auth, and switch away from pickle if possible.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| if len(local_indexes) == 1: | ||
| # The unsqueeze op make the shape from n to (1, n) | ||
| gathered_item = self.field_data[field][local_indexes[0]] | ||
| if not isinstance(gathered_item, torch.Tensor): | ||
| result[field] = NonTensorStack(gathered_item) | ||
| else: | ||
| result[field] = gathered_item.unsqueeze(0) | ||
| else: | ||
| gathered_items = list(itemgetter(*local_indexes)(self.field_data[field])) | ||
|
|
||
| if gathered_items: |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Validate local_indexes on GET; guard None slots.
get_data doesn’t bound-check local_indexes; accessing unset slots may return None and break stacking.
@@ def get_data(self, fields: list[str], local_indexes: list[int]) -> TensorDict[str, list]: - for field in fields: + # Validate indexes once + for idx in local_indexes: + if idx < 0 or idx >= self.storage_size: + raise ValueError( + f"StorageUnitData get_data operation receive invalid local_index: {idx} beyond " + f"storage_size: {self.storage_size}" + ) + for field in fields: @@ - gathered_item = self.field_data[field][local_indexes[0]] + gathered_item = self.field_data[field][local_indexes[0]] if not isinstance(gathered_item, torch.Tensor): - result[field] = NonTensorStack(gathered_item) + result[field] = NonTensorStack(gathered_item) # single element else: result[field] = gathered_item.unsqueeze(0) @@ - if gathered_items: + if gathered_items: + # Filter out None slots defensively + if any(x is None for x in gathered_items): + raise ValueError(f"Field '{field}' contains empty slots at {local_indexes}") all_tensors = all(isinstance(x, torch.Tensor) for x in gathered_items)
Also applies to: 129-147
In transfer_queue/storage/simple_backend.py around lines 86-96 (and similarly
for 129-147), validate local_indexes before using them: ensure every index is an
int within bounds of self.field_data[field] (raise IndexError if any index is
out-of-range), and when gathering entries skip or filter out slots that are None
so stacking operations don't receive None values; then build result[field] from
the filtered items (use NonTensorStack for non-tensor items and
torch.stack/unsqueeze for tensors) and handle the case where all selected slots
are None by returning an appropriate empty value or raising a clear error.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| for f, values in extracted_data.items(): | ||
| if f not in self.field_data: | ||
| self.field_data[f] = [None] * self.storage_size | ||
|
|
||
| for i, idx in enumerate(local_indexes): | ||
| if idx < 0 or idx >= self.storage_size: | ||
| raise ValueError( | ||
| f"StorageUnitData put_data operation receive invalid local_index: {idx} beyond " | ||
| f"storage_size: {self.storage_size}" | ||
| ) | ||
|
|
||
| self.field_data[f][idx] = values[i] | ||
|
|
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Prevent length mismatch and index errors in put_data.
When values length ≠ local_indexes length, IndexError/incorrect writes occur. Also avoid enumerate-coupling and reuse the same index validation.
Apply:
@@ - for f, values in extracted_data.items(): + for f, values in extracted_data.items(): if f not in self.field_data: self.field_data[f] = [None] * self.storage_size - for i, idx in enumerate(local_indexes): + if len(values) != len(local_indexes): + raise ValueError( + f"put_data length mismatch for field '{f}': " + f"{len(values)} values for {len(local_indexes)} indexes" + ) + for idx, value in zip(local_indexes, values): if idx < 0 or idx >= self.storage_size: raise ValueError( f"StorageUnitData put_data operation receive invalid local_index: {idx} beyond " f"storage_size: {self.storage_size}" ) - self.field_data[f][idx] = values[i] + self.field_data[f][idx] = value
In transfer_queue/storage/simple_backend.py around lines 115 to 127, guard
against mismatched lengths and unsafe indexing in put_data by first verifying
that len(values) == len(local_indexes) and raising a clear ValueError if not;
then iterate over pairs using zip(local_indexes, values) (not enumerate) so each
value is matched to its index, validate each idx is within [0,
self.storage_size) before assignment, and perform the assignment into
self.field_data[f][idx] only after the bounds check (create the field list if
missing as currently done).
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| try: | ||
| request_msg = ZMQMessage.deserialize(serialized_msg) | ||
| operation = request_msg.request_type | ||
| logger.debug(f"[{self.zmq_server_info.id}]: receive operation: {operation}, message: {request_msg}") | ||
|
|
||
| if operation == ZMQRequestType.PUT_DATA: | ||
| response_msg = self._handle_put(request_msg) | ||
| elif operation == ZMQRequestType.GET_DATA: | ||
| response_msg = self._handle_get(request_msg) | ||
| elif operation == ZMQRequestType.CLEAR_DATA: | ||
| response_msg = self._handle_clear(request_msg) | ||
| else: | ||
| response_msg = ZMQMessage.create( | ||
| request_type=ZMQRequestType.PUT_GET_OPERATION_ERROR, | ||
| sender_id=self.zmq_server_info.id, | ||
| body={ | ||
| "message": f"Storage unit id #{self.zmq_server_info.id} " | ||
| f"receive invalid operation: {operation}." | ||
| }, | ||
| ) | ||
| except Exception as e: | ||
| response_msg = ZMQMessage.create( | ||
| request_type=ZMQRequestType.PUT_GET_ERROR, | ||
| sender_id=self.zmq_server_info.id, | ||
| body={ | ||
| "message": f"Storage unit id #{self.zmq_server_info.id} occur error in processing " | ||
| f"put/get/clear request, detail error message: {str(e)}." | ||
| }, | ||
| ) | ||
|
|
||
| self.put_get_socket.send_multipart([identity, response_msg.serialize()]) |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Unsafe network deserialization via pickle.
Deserializing pickled messages from sockets enables code execution if an attacker can connect. Consider msgpack/JSON + schema validation; if pickle is unavoidable, restrict bind addresses, add ZMQ CURVE/auth, and authenticate messages (HMAC).
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| @contextmanager | ||
| def limit_pytorch_auto_parallel_threads(target_num_threads: Optional[int] = None): | ||
| """Prevent PyTorch from overdoing the automatic parallelism during torch.stack() operation""" | ||
| pytorch_current_num_threads = torch.get_num_threads() | ||
| logical_cores = psutil.cpu_count(logical=True) | ||
| physical_cores = psutil.cpu_count(logical=False) | ||
|
|
||
| if target_num_threads is None: | ||
| # auto determine target_num_threads | ||
| if physical_cores >= 16: | ||
| target_num_threads = 16 | ||
| else: | ||
| target_num_threads = physical_cores | ||
|
|
||
| if target_num_threads > logical_cores: | ||
| raise RuntimeError( | ||
| f"target_num_threads {target_num_threads} should not exceed total logical CPU cores {logical_cores}" | ||
| ) | ||
|
|
||
| if pytorch_current_num_threads <= target_num_threads: | ||
| # No need to change settings | ||
| yield | ||
| else: | ||
| torch.set_num_threads(target_num_threads) | ||
| try: | ||
| yield | ||
| finally: | ||
| # Restore the original number of threads | ||
| torch.set_num_threads(pytorch_current_num_threads) |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Critical: Handle potential None values from psutil.cpu_count().
The function has a critical bug: psutil.cpu_count() can return None on systems where the CPU count cannot be determined. This will cause runtime errors at lines 113, 116, and 118 when performing comparisons or arithmetic with None.
Apply this diff to handle None values safely:
@contextmanager
def limit_pytorch_auto_parallel_threads(target_num_threads: Optional[int] = None):
"""Prevent PyTorch from overdoing the automatic parallelism during torch.stack() operation"""
pytorch_current_num_threads = torch.get_num_threads()
logical_cores = psutil.cpu_count(logical=True)
physical_cores = psutil.cpu_count(logical=False)
+
+ if logical_cores is None or physical_cores is None:
+ raise RuntimeError(
+ "Unable to determine CPU core count on this system"
+ )
if target_num_threads is None:
# auto determine target_num_threads
if physical_cores >= 16:
target_num_threads = 16
else:
target_num_threads = physical_cores
if target_num_threads > logical_cores:
raise RuntimeError(
f"target_num_threads {target_num_threads} should not exceed total logical CPU cores {logical_cores}"
)In transfer_queue/utils/utils.py around lines 104 to 132,
psutil.cpu_count(logical=True/False) can return None which will break the
comparisons and arithmetic; coalesce None to safe integer fallbacks (e.g., use
torch.get_num_threads() or 1 for logical_cores, and for physical_cores use the
logical_cores fallback if None), ensure target_num_threads is always an int
before comparing or assigning (use min/max as appropriate), and update the
check/assignment logic so you never compare None to an int and always restore
the original torch thread count in the finally block.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| def __init__(self, role: TransferQueueRole, id: str, ip: str, ports: dict[str, str]): | ||
| self.role = role | ||
| self.id = id | ||
| self.ip = ip | ||
| self.ports = ports | ||
|
|
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Port type mismatch: ZMQServerInfo.ports expects str but callers pass ints
Downstream (e.g., SimpleStorageUnit) builds ports using get_free_port(), which returns an int; current signature says dict[str, str]. Align the type to int to avoid typing/tooling issues.
Apply this diff:
- def __init__(self, role: TransferQueueRole, id: str, ip: str, ports: dict[str, str]): + def __init__(self, role: TransferQueueRole, id: str, ip: str, ports: dict[str, int]): self.role = role self.id = id self.ip = ip self.ports = ports
In transfer_queue/utils/zmq_utils.py around lines 69 to 74, the ZMQServerInfo
__init__ signature declares ports: dict[str, str] but callers (e.g.,
SimpleStorageUnit) pass ints from get_free_port(); change the type annotation to
ports: dict[str, int] (or more generally Mapping[str, int]) to match callers,
update any related type hints/returns/usages in the module to accept ints, and
run type checks to ensure no remaining mismatches.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
Signed-off-by: 0oshowero0 <[email protected]>
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Copilot reviewed 24 out of 25 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| all_samples = [] | ||
| for chunk in data: | ||
| all_samples.extend(chunk.samples) | ||
| all_samples = list(itertools.chain.from_iterable(chunk.samples for chunk in data)) |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
This creates an intermediate generator that is immediately consumed into a list. For better performance, use all_samples = [sample for chunk in data for sample in chunk.samples] which is more efficient and readable.
| all_samples = list(itertools.chain.from_iterable(chunk.samples for chunk in data)) | |
| all_samples = [sample for chunk in data for sample in chunk.samples] |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| """ | ||
| self.storage_manager = TransferQueueStorageManagerFactory.create(manager_type, config) | ||
|
|
||
| # TODO (TQStorage): Provide a general dynamic socket function for both Client & Storage @huazhong. |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
The comment on line 91 states this is a TODO for providing a general dynamic socket function for both Client & Storage, but this is a static method that doesn't accept target_role anymore. The TODO should be updated or resolved since the implementation has changed from the storage manager version.
| # TODO (TQStorage): Provide a general dynamic socket function for both Client & Storage @huazhong. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| data_fields=["input_ids", "attention_mask", "generate_sequences_ids"], | ||
| batch_size=config.global_batch_size, | ||
| global_step=0, | ||
| global_step=step, |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
The global_step parameter was changed from hardcoded 0 to step, but this may cause issues if the same step is used for multiple operations within the loop. Verify that this change aligns with the intended behavior of the data retrieval logic.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
transfer_queue/storage/simple_backend.py (4)
173-173: UUID collision risk with truncated hex.Using only 8 hex characters from UUID (
uuid4().hex[:8]) creates collision risk in deployments with many storage units (birthday paradox: ~1% chance at ~300 units).Use the full UUID hex or at least 16 characters:
- self.storage_unit_id = f"TQ_STORAGE_UNIT_{uuid4().hex[:8]}" + self.storage_unit_id = f"TQ_STORAGE_UNIT_{uuid4().hex}"
86-103: Validate local_indexes and handle None slots defensively.The
get_datamethod doesn't validate thatlocal_indexesare within bounds before accessingself.field_data[field][local_indexes[0]]or usingitemgetter. This can raiseIndexErrorfor out-of-range indexes. Additionally, if storage slots containNone(e.g., after a partial clear or before data is written),itemgetterwill returnNonevalues that can break stacking operations.Apply this diff to add bounds checking and filter out None values:
def get_data(self, fields: list[str], local_indexes: list[int]) -> TensorDict[str, list]: """ Get data from storage unit according to given fields and local_indexes. """ result: dict[str, list] = {} + + # Validate indexes once upfront + for idx in local_indexes: + if idx < 0 or idx >= self.storage_size: + raise ValueError( + f"StorageUnitData get_data operation receive invalid local_index: {idx} beyond " + f"storage_size: {self.storage_size}" + ) for field in fields: # Validate field name if field not in self.field_data: raise ValueError( f"StorageUnitData get_data operation receive invalid field: {field} beyond {self.field_data.keys()}" ) if len(local_indexes) == 1: gathered_item = self.field_data[field][local_indexes[0]] + if gathered_item is None: + raise ValueError(f"Field '{field}' at local_index {local_indexes[0]} is empty/None") if not isinstance(gathered_item, torch.Tensor): result[field] = NonTensorStack(gathered_item) else: result[field] = gathered_item.unsqueeze(0) else: gathered_items = list(itemgetter(*local_indexes)(self.field_data[field])) + + # Check for None values + if any(x is None for x in gathered_items): + none_indexes = [idx for idx, x in zip(local_indexes, gathered_items) if x is None] + raise ValueError(f"Field '{field}' contains empty slots at indexes: {none_indexes}") if gathered_items: all_tensors = all(isinstance(x, torch.Tensor) for x in gathered_items) if all_tensors: result[field] = torch.nested.as_nested_tensor(gathered_items) else: result[field] = NonTensorStack(*gathered_items) return TensorDict(result)
115-127: Prevent length mismatch and avoid enumerate-coupling in put_data.When
len(values)≠len(local_indexes), the current code usingenumerate(local_indexes)and indexingvalues[i]will either raiseIndexErroror write incorrect data. The bounds check happens per-index but after the mismatch has already caused issues.Apply this diff to validate lengths upfront and use
zip:def put_data(self, field_data: TensorDict[str, Any], local_indexes: list[int]) -> None: """ Put or update data into storage unit according to given field_data and local_indexes. """ extracted_data = dict(field_data) for f, values in extracted_data.items(): if f not in self.field_data: self.field_data[f] = [None] * self.storage_size + + if len(values) != len(local_indexes): + raise ValueError( + f"put_data length mismatch for field '{f}': " + f"{len(values)} values for {len(local_indexes)} indexes" + ) - for i, idx in enumerate(local_indexes): + for idx, value in zip(local_indexes, values): if idx < 0 or idx >= self.storage_size: raise ValueError( f"StorageUnitData put_data operation receive invalid local_index: {idx} beyond " f"storage_size: {self.storage_size}" ) - self.field_data[f][idx] = values[i] + self.field_data[f][idx] = value
216-246: Unsafe network deserialization via pickle.Deserializing pickled messages from sockets (line 217:
ZMQMessage.deserialize(serialized_msg)) enables arbitrary code execution if an attacker can connect to the storage unit's ZMQ endpoint. This is a known Python security issue with pickle over the network.Consider these mitigations:
- Preferred: Replace pickle with a safe serialization format (msgpack, JSON + schema validation)
- If pickle unavoidable:
- Restrict bind addresses to trusted networks only
- Add ZMQ CURVE authentication
- Implement message authentication (HMAC)
- Document the security model and deployment restrictions
Also see: Python pickle security documentation
transfer_queue/storage/managers/simple_backend_manager.py (2)
120-128: Missing port validation when building address.Line 126 builds the address using
server_info.ports.get(socket_name)without verifying the port exists. If the port is missing, this producestcp://127.0.0.1:None, leading to obscure connection errors.Add explicit validation:
server_info = self.storage_unit_infos.get(server_key) if not server_info: raise RuntimeError(f"Server {server_key} not found in registered servers") context = zmq.asyncio.Context() - address = f"tcp://{server_info.ip}:{server_info.ports.get(socket_name)}" + port = server_info.ports.get(socket_name) + if port is None: + raise ValueError( + f"StorageUnit {server_info.id} missing port '{socket_name}' in ports={server_info.ports}" + ) + address = f"tcp://{server_info.ip}:{port}" identity = f"{self.storage_manager_id}_to_{server_info.id}_{uuid4().hex[:8]}".encode() sock = create_zmq_socket(context, zmq.DEALER, identity=identity)
211-221: Handle empty field_data to avoid NonTensorStack construction errors.If
transfer_data["field_data"][field]is an empty list,NonTensorStack(*[])is called with no arguments, which may raise an error depending on the NonTensorStack implementation.Add defensive handling for empty field data:
tensordict_data = TensorDict( { field: ( - torch.nested.as_nested_tensor(transfer_data["field_data"][field]) - if transfer_data["field_data"][field] - and all(isinstance(x, torch.Tensor) for x in transfer_data["field_data"][field]) - else NonTensorStack(*transfer_data["field_data"][field]) + self._to_stackable(transfer_data["field_data"][field]) ) for field in transfer_data["field_data"] } ) + + def _to_stackable(self, field_data_list): + """Convert field data to stackable format, handling empty lists.""" + if not field_data_list: + return NonTensorStack() # Handle empty case + if all(isinstance(x, torch.Tensor) for x in field_data_list): + return torch.nested.as_nested_tensor(field_data_list) + return NonTensorStack(*field_data_list)Add this helper method to the
AsyncSimpleStorageManagerclass to improve readability and handle edge cases.
transfer_queue/storage/simple_backend.py (1)
260-262: Clarify field_data parameter type in docstring.The comment at line 260 states
field_data should be a TensorDict, which is correct. However, past comments noted the format description was inconsistent with usage.The implementation is correct. For clarity, you could enhance the docstring:
""" Handle put request, add or update data into storage unit. Args: - data_parts: ZMQMessage from client. + data_parts: ZMQMessage from client containing: + - local_indexes: list[int] - target storage positions + - data: TensorDict - field data to store Returns: Put data success response ZMQMessage. """transfer_queue/storage/managers/simple_backend_manager.py (1)
287-303: Review nested comprehension complexity.The nested conditional in lines 289-299 for building
tensor_datais complex. While functionally correct, it could benefit from extraction into a helper method for readability.Consider extracting to a helper:
def _stack_field_data(self, field: str, v: list) -> torch.Tensor | NonTensorStack: """Stack field data with appropriate nesting.""" if not v: return NonTensorStack() if not all(isinstance(item, torch.Tensor) for item in v): return NonTensorStack(*v) # All tensors - check if same shape if all(item.shape == v[0].shape for item in v): return torch.stack(torch.nested.as_nested_tensor(v).unbind()) else: return torch.nested.as_nested_tensor(v)Then simplify the dict comprehension:
tensor_data = { field: self._stack_field_data(field, v) for field, v in ordered_data.items() }
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
tests/test_async_simple_storage_manager.py (1 hunks)tests/test_client.py (10 hunks)transfer_queue/client.py (12 hunks)transfer_queue/storage/managers/simple_backend_manager.py (1 hunks)transfer_queue/storage/simple_backend.py (1 hunks)transfer_queue/metadata.py (3)
BatchMeta(136-417)FieldMeta(28-45)SampleMeta(49-132)transfer_queue/storage/managers/simple_backend_manager.py (7)
AsyncSimpleStorageManager(37-415)_put_to_single_storage_unit(205-241)_get_from_single_storage_unit(306-337)_clear_single_storage_unit(366-388)put_data(163-202)get_data(243-303)clear_data(339-363)transfer_queue/utils/utils.py (1)
TransferQueueRole(36-39)transfer_queue/utils/zmq_utils.py (5)
ZMQMessage(91-131)ZMQRequestType(31-65)ZMQServerInfo(68-87)create(100-114)serialize(116-118)transfer_queue/storage/managers/base.py (5)
_connect_to_controller(55-82)notify_data_update(172-256)put_data(259-260)get_data(263-264)clear_data(267-268)
transfer_queue/metadata.py (3)
BatchMeta(136-417)global_indexes(167-169)batch_index(70-72)transfer_queue/storage/managers/base.py (3)
TransferQueueStorageManager(40-268)notify_data_update(172-256)get_data(263-264)transfer_queue/storage/simple_backend.py (4)
StorageMetaGroup(355-449)get_transfer_data(389-428)get_data(66-103)add_sample_meta(365-368)transfer_queue/utils/utils.py (1)
limit_pytorch_auto_parallel_threads(105-132)transfer_queue/utils/zmq_utils.py (7)
ZMQMessage(91-131)ZMQRequestType(31-65)ZMQServerInfo(68-87)create_zmq_socket(140-170)create(100-114)serialize(116-118)deserialize(121-131)
transfer_queue/utils/utils.py (1)
TransferQueueRole(36-39)transfer_queue/utils/zmq_utils.py (1)
ZMQServerInfo(68-87)transfer_queue/client.py (3)
initialize_storage_manager(73-89)get_data(547-556)TransferQueueClient(485-564)transfer_queue/storage/managers/simple_backend_manager.py (3)
put_data(163-202)get_data(243-303)clear_data(339-363)transfer_queue/storage/simple_backend.py (2)
put_data(105-126)get_data(66-103)transfer_queue/storage/managers/base.py (3)
put_data(259-260)get_data(263-264)clear_data(267-268)
transfer_queue/controller.py (3)
TransferQueueController(55-750)clear(737-750)get_zmq_server_info(729-735)transfer_queue/metadata.py (2)
BatchMeta(136-417)size(162-164)transfer_queue/storage/simple_backend.py (6)
SimpleStorageUnit(150-351)put_data(105-126)size(431-433)get_data(66-103)clear(128-146)get_zmq_server_info(345-351)transfer_queue/storage/managers/base.py (4)
TransferQueueStorageManager(40-268)put_data(259-260)get_data(263-264)clear_data(267-268)transfer_queue/storage/managers/factory.py (2)
TransferQueueStorageManagerFactory(20-40)create(35-40)transfer_queue/utils/zmq_utils.py (5)
ZMQServerInfo(68-87)create(100-114)create_zmq_socket(140-170)ZMQMessage(91-131)ZMQRequestType(31-65)transfer_queue/storage/managers/simple_backend_manager.py (6)
wrapper(112-157)put_data(163-202)get_data(243-303)clear_data(339-363)close(398-415)get_zmq_server_info(390-396)
transfer_queue/metadata.py (2)
SampleMeta(49-132)batch_index(70-72)transfer_queue/utils/zmq_utils.py (9)
ZMQMessage(91-131)ZMQRequestType(31-65)ZMQServerInfo(68-87)create_zmq_socket(140-170)get_free_port(134-137)to_addr(75-76)deserialize(121-131)create(100-114)serialize(116-118)
transfer_queue/storage/simple_backend.py (1)
354-449: LGTM: StorageMetaGroup design is clean.The
StorageMetaGroupclass provides a well-structured abstraction for grouping samples by storage unit. The helper methods (get_batch_indexes,get_global_indexes,get_field_names,get_transfer_data) cleanly separate concerns and the docstrings with examples are excellent.transfer_queue/client.py (5)
54-71: LGTM: Single-controller initialization with validation.The refactored constructor properly validates the
controller_infoparameter with type checking and null checks. The logging provides good visibility into the registration.
289-310: Storage manager initialization check is present.The
async_putmethod now includes the requested validation check (lines 289-293) to ensurestorage_manageris initialized before use. The error message is clear and actionable.
341-352: Storage manager check properly added in async_get_data.Lines 341-345 implement the validation check that was flagged in past reviews. The implementation matches the pattern used in
async_put.
364-383: Storage manager and controller checks properly implemented.The
async_clearmethod now includes both storage manager validation (lines 364-368) and controller validation (lines 370-371) as requested in past reviews.
475-483: Good defensive cleanup in close().The
closemethod properly handles missing attributes and exceptions during cleanup, preventing resource leaks.tests/test_async_simple_storage_manager.py (4)
35-91: Well-designed test fixture with proper ZMQ mocking.The
mock_async_storage_managerfixture properly mocks the ZMQ handshake while preserving the essential manager state (storage_unit_infos, mapping functions). This allows testing the business logic without ZMQ complexity.
93-159: Comprehensive test coverage for basic operations.The test validates initialization, put_data, get_data, and clear_data operations with proper assertions. Mocking the internal methods (
_put_to_single_storage_unit, etc.) is appropriate for unit testing the orchestration layer.
161-246: Excellent validation of round-robin mapping logic.This test explicitly validates the distribution algorithm with 3 storage units, covering multiple cycles. The assertions clearly document the expected behavior (global_index → storage_unit and global_index → local_index mappings).
248-339: Good error propagation testing.The test verifies that exceptions from
put_dataandget_datapropagate correctly, whileclear_datatolerates exceptions (due toreturn_exceptions=True). This validates the intended error handling semantics.transfer_queue/storage/managers/simple_backend_manager.py (1)
418-550: Well-documented data flow helpers with excellent examples.The
get_transfer_data,_add_field_data, andbuild_storage_meta_groupsfunctions have outstanding documentation. The docstrings with examples clearly explain the criticalbatch_index→ data extraction flow. This will help maintainers understand the data distribution logic.tests/test_client.py (4)
69-76: LGTM: Proper use of TransferQueueRole enum.The ZMQServerInfo construction correctly uses
TransferQueueRole.CONTROLLERinstead of string-based roles, aligning with the refactored architecture.
271-278: Patch path uses full module location - good practice.Line 272 correctly patches
transfer_queue.storage.managers.simple_backend_manager.AsyncSimpleStorageManager._connect_to_controller, targeting the actual definition location rather than the re-export. This is more reliable for mocking.
265-294: Test setup properly mocks storage manager operations.The client_setup fixture appropriately mocks the handshake and all storage manager data operations (put_data, get_data, clear_data) to isolate client logic from ZMQ I/O.
359-410: Comprehensive multi-storage test with proper cleanup.The
test_single_controller_multiple_storagesfunction validates the single-controller architecture with 3 storage units and ensures proper cleanup in the finally block.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
Signed-off-by: 0oshowero0 <[email protected]>
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
transfer_queue/metadata.py (1)
222-230: Fix logic bug and performance issue in add_fields.Two issues:
Logic bug: The validation at lines 222-224 only runs if
fieldsis truthy, but the for loop at line 225 runs unconditionally. Iffieldsis an empty list, line 226 will raiseIndexErrorwhen accessingfields[idx].Performance issue: Lines 228-230 update batch-level caches inside the loop, recomputing on every iteration. These should execute once after the loop completes.
Apply this diff:
fields = _extract_field_metas(tensor_dict, set_all_ready) if fields: if len(self.samples) != len(fields): raise ValueError(f"add_fields length mismatch: samples={len(self.samples)} vs fields={len(fields)}") - for idx, sample in enumerate(self.samples): - sample.add_fields(fields=fields[idx]) - - # Update batch-level fields cache - object.__setattr__(self, "_field_names", sorted(self.samples[0].field_names)) - object.__setattr__(self, "_is_ready", all(sample.is_ready for sample in self.samples)) + + for idx, sample in enumerate(self.samples): + sample.add_fields(fields=fields[idx]) + + # Update batch-level fields cache + object.__setattr__(self, "_field_names", sorted(self.samples[0].field_names)) + object.__setattr__(self, "_is_ready", all(sample.is_ready for sample in self.samples)) return self
transfer_queue/metadata.py (3)
89-92: Docstring still contradicts implementation.The docstring claims default values will be used for missing dtype/shape/production_status, but the implementation (line 93) simply merges the provided fields dictionary without any defaulting logic.
298-298: Consider list comprehension for better readability.As previously noted, a list comprehension would be more direct and readable than creating an intermediate iterator.
300-300: Breaking change: ChainMap reverses precedence order.The current implementation uses
dict(ChainMap(*(chunk.extra_info for chunk in data))), which gives precedence to the first chunk's extra_info values when keys overlap.If the original code used
update()in a loop, later chunks would overwrite earlier ones (last-wins semantics). This is a breaking behavioral change.To restore last-wins semantics, apply this diff:
- merged_extra_info = dict(ChainMap(*(chunk.extra_info for chunk in data))) + merged_extra_info = {} + for chunk in data: + merged_extra_info.update(chunk.extra_info)Or if first-wins is intentional, document this explicitly in the method docstring.
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
tests/test_client.py (10 hunks)transfer_queue/metadata.py (8 hunks)transfer_queue/utils/utils.py (1)
ProductionStatus(43-46)
transfer_queue/utils/utils.py (1)
TransferQueueRole(36-39)transfer_queue/utils/zmq_utils.py (1)
ZMQServerInfo(68-87)transfer_queue/client.py (3)
initialize_storage_manager(73-89)get_data(547-556)TransferQueueClient(485-564)transfer_queue/storage/managers/simple_backend_manager.py (3)
put_data(163-202)get_data(243-303)clear_data(339-363)
transfer_queue/metadata.py (1)
430-460: LGTM! Clean field metadata extraction.The implementation correctly extracts per-sample field metadata from the TensorDict, gracefully handling missing dtype/shape attributes with
Nonedefaults. The list comprehension is clear and efficient.tests/test_client.py (2)
270-293: LGTM! Clean mocking approach.The storage manager mocking strategy is well-structured:
- Uses the full module path for patching
_connect_to_controller(consistent with past feedback)- Mocks ZMQ operations to avoid real network I/O in tests
- Properly initializes the manager before replacing its methods
358-410: LGTM! Test properly adapted to single-controller architecture.The test correctly reflects the new API:
- Single controller with multiple storage units
- Consistent mocking approach with the fixture
- Proper resource cleanup in the finally block
- Appropriate verification of controller setup
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| print(f"MockController ERROR: {e}") | ||
| raise |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Inconsistent error handling between MockController and MockStorage.
MockController (lines 119-120) always raises exceptions after logging, but MockStorage (lines 214-218) only raises when self.running is False. When running is True, MockStorage swallows exceptions after logging them. This inconsistency can mask test failures and make debugging harder.
For test reliability, consider making the error handling consistent by having both mocks always raise exceptions during test execution:
Apply this diff to align MockStorage error handling with MockController:
except Exception as e: - if self.running: - print(f"MockStorage running exception: {e}") - else: - print(f"MockStorage ERROR: {e}") - raise + print(f"MockStorage ERROR: {e}") + raise
Also applies to: 214-218
In tests/test_client.py around lines 119-120 and 214-218, MockController
currently logs an exception then re-raises it while MockStorage logs and only
re-raises when self.running is False; make handling consistent by always
re-raising exceptions after logging in MockStorage (remove or replace the
conditional that swallows errors when self.running is True), so both mocks log
the error and then raise the original exception to surface failures during
tests.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
Successfully merging this pull request may close these issues.
There was an error while loading. Please reload this page.