Skip to content

Commit 170b8ed

Browse files
authored
[fix,refactor] Merge update_data_status thread/socket into request_handle to eliminate concurrency conflicts (#116)
Previously, _update_data_status and _process_request ran on two separate threads listening on two different ROUTER sockets. When used in async frameworks, NOTIFY_DATA_UPDATE and metadata requests (GET_META, CLEAR_META, etc.) could interleave and cause data processing conflicts on shared partition state. This change consolidates everything onto request_handle_socket and the single _process_request thread, so all controller requests are serialized naturally: - Drop data_status_update_socket and its port; ZMQServerInfo.ports now exposes only handshake_socket and request_handle_socket. - Remove _start_process_update_data_status and _update_data_status; move the NOTIFY_DATA_UPDATE branch into _process_request. - Remove data_status_lock logics. - Update storage manager to send NOTIFY_DATA_UPDATE to request_handle_socket. - Clean up data_status_update_socket port entries in test fixtures. Note: this is a breaking change to the controller-storage wire protocol (port name removed). All storage managers must be upgraded together. --------- Signed-off-by: yxstev <zhangyixiang9@huawei.com>
1 parent 136ec16 commit 170b8ed

4 files changed

Lines changed: 68 additions & 132 deletions

File tree

tests/test_async_simple_storage_manager.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ async def mock_async_storage_manager():
5353
role=Role.CONTROLLER,
5454
id="controller_0",
5555
ip="127.0.0.1",
56-
ports={"handshake_socket": 12347, "data_status_update_socket": 12348},
56+
ports={"handshake_socket": 12347},
5757
)
5858

5959
config = {
@@ -68,7 +68,6 @@ async def mock_async_storage_manager():
6868
manager.config = config
6969
manager.controller_info = controller_info
7070
manager.storage_unit_infos = storage_unit_infos
71-
manager.data_status_update_socket = None
7271
manager.controller_handshake_socket = None
7372
manager.zmq_context = None
7473

@@ -158,7 +157,7 @@ async def test_async_storage_manager_error_handling():
158157
role=Role.CONTROLLER,
159158
id="controller_0",
160159
ip="127.0.0.1",
161-
ports={"handshake_socket": 12346, "data_status_update_socket": 12347},
160+
ports={"handshake_socket": 12346},
162161
)
163162

164163
config = {
@@ -257,7 +256,6 @@ async def test_get_data_routes_from_hash():
257256
manager.storage_manager_id = "test_get"
258257
manager.storage_unit_infos = storage_unit_infos
259258
manager.controller_info = None
260-
manager.data_status_update_socket = None
261259
manager.controller_handshake_socket = None
262260
manager.zmq_context = None
263261

@@ -310,7 +308,6 @@ async def test_clear_data_routes_from_hash():
310308
manager.storage_manager_id = "test_clear"
311309
manager.storage_unit_infos = storage_unit_infos
312310
manager.controller_info = None
313-
manager.data_status_update_socket = None
314311
manager.controller_handshake_socket = None
315312
manager.zmq_context = None
316313

@@ -361,7 +358,6 @@ async def test_hash_routing_stable_across_batch_sizes():
361358
manager.storage_manager_id = "test_hash_batch"
362359
manager.storage_unit_infos = storage_unit_infos
363360
manager.controller_info = None
364-
manager.data_status_update_socket = None
365361
manager.controller_handshake_socket = None
366362
manager.zmq_context = None
367363

@@ -422,7 +418,6 @@ async def test_hash_routing_stable_reversed_order():
422418
manager.storage_manager_id = "test_hash_order"
423419
manager.storage_unit_infos = storage_unit_infos
424420
manager.controller_info = None
425-
manager.data_status_update_socket = None
426421
manager.controller_handshake_socket = None
427422
manager.zmq_context = None
428423

tests/test_ray_p2p.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ def create_mock_controller():
6060
ip="127.0.0.1",
6161
ports={
6262
"request_handle_socket": 9981,
63-
"data_status_update_socket": 9982,
6463
"handshake_socket": 9983,
6564
},
6665
)

transfer_queue/controller.py

Lines changed: 65 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from dataclasses import dataclass, field
2121
from itertools import groupby
2222
from operator import itemgetter
23-
from threading import Lock, Thread
23+
from threading import Thread
2424
from typing import TYPE_CHECKING, Any
2525
from uuid import uuid4
2626

@@ -361,10 +361,6 @@ class DataPartitionStatus:
361361
keys_mapping: dict[str, int] = field(default_factory=dict) # key -> global_idx
362362
revert_keys_mapping: dict[int, str] = field(default_factory=dict) # global_idx -> key
363363

364-
# Threading lock for concurrency control; only for preventing mask operation error when expanding production_status.
365-
# No need to strictly lock for every read/write operation since freshness is not critical.
366-
data_status_lock: Lock = field(default_factory=Lock)
367-
368364
# Dynamic configuration - these are computed from the current state
369365
@property
370366
def total_samples_num(self) -> int:
@@ -409,8 +405,7 @@ def register_pre_allocated_indexes(self, allocated_indexes: list[int]):
409405
max_sample_idx = max(allocated_indexes)
410406
required_samples = max_sample_idx + 1
411407

412-
with self.data_status_lock:
413-
self.ensure_samples_capacity(required_samples)
408+
self.ensure_samples_capacity(required_samples)
414409

415410
logger.debug(f"Pre-allocated indexes in {self.partition_id}: {allocated_indexes}")
416411

@@ -526,9 +521,8 @@ def update_production_status(
526521
max_sample_idx = max(global_indices) if global_indices else -1
527522
required_samples = max_sample_idx + 1
528523

529-
with self.data_status_lock:
530-
# Ensure we have enough rows
531-
self.ensure_samples_capacity(required_samples)
524+
# Ensure we have enough rows
525+
self.ensure_samples_capacity(required_samples)
532526

533527
# Register new fields if needed
534528
new_fields = [f for f in field_names if f not in self.field_name_mapping]
@@ -538,14 +532,12 @@ def update_production_status(
538532
self.field_name_mapping[f] = len(self.field_name_mapping)
539533

540534
required_fields = len(self.field_name_mapping)
541-
with self.data_status_lock:
542-
self.ensure_fields_capacity(required_fields)
535+
self.ensure_fields_capacity(required_fields)
543536

544-
with self.data_status_lock:
545-
# Update production status
546-
if self.production_status is not None and global_indices and field_names:
547-
field_indices = [self.field_name_mapping.get(f) for f in field_names]
548-
self.production_status[torch.tensor(global_indices)[:, None], torch.tensor(field_indices)] = 1
537+
# Update production status
538+
if self.production_status is not None and global_indices and field_names:
539+
field_indices = [self.field_name_mapping.get(f) for f in field_names]
540+
self.production_status[torch.tensor(global_indices)[:, None], torch.tensor(field_indices)] = 1
549541

550542
# Update field metadata
551543
self._update_field_metadata(global_indices, field_schema, custom_backend_meta)
@@ -641,8 +633,7 @@ def get_consumption_status(self, task_name: str, mask: bool = False) -> tuple[Te
641633
if partition_global_index.numel() == 0:
642634
empty_status = self.consumption_status[task_name].new_zeros(0)
643635
return partition_global_index, empty_status
644-
with self.data_status_lock:
645-
self.ensure_samples_capacity(max(partition_global_index) + 1)
636+
self.ensure_samples_capacity(max(partition_global_index) + 1)
646637
consumption_status = self.consumption_status[task_name][partition_global_index]
647638
else:
648639
consumption_status = self.consumption_status[task_name]
@@ -730,23 +721,22 @@ def scan_data_status(self, field_names: list[str], task_name: str) -> list[int]:
730721
if field_name not in self.field_name_mapping:
731722
return []
732723

733-
with self.data_status_lock:
734-
row_mask = torch.ones(self.allocated_samples_num, dtype=torch.bool)
724+
row_mask = torch.ones(self.allocated_samples_num, dtype=torch.bool)
735725

736-
# Apply consumption filter (exclude already consumed samples)
737-
_, consumption_status = self.get_consumption_status(task_name, mask=False)
738-
if consumption_status is not None:
739-
unconsumed_mask = consumption_status == 0
740-
row_mask &= unconsumed_mask
726+
# Apply consumption filter (exclude already consumed samples)
727+
_, consumption_status = self.get_consumption_status(task_name, mask=False)
728+
if consumption_status is not None:
729+
unconsumed_mask = consumption_status == 0
730+
row_mask &= unconsumed_mask
741731

742-
# Create column mask for requested fields
743-
col_mask = torch.zeros(self.allocated_fields_num, dtype=torch.bool)
744-
field_indices = [self.field_name_mapping[field] for field in field_names]
745-
if field_indices:
746-
col_mask[field_indices] = True
732+
# Create column mask for requested fields
733+
col_mask = torch.zeros(self.allocated_fields_num, dtype=torch.bool)
734+
field_indices = [self.field_name_mapping[field] for field in field_names]
735+
if field_indices:
736+
col_mask[field_indices] = True
747737

748-
# Filter production status by masks
749-
relevant_status = self.production_status[row_mask][:, col_mask]
738+
# Filter production status by masks
739+
relevant_status = self.production_status[row_mask][:, col_mask]
750740

751741
# Check if all required fields are ready for each sample
752742
all_fields_ready = torch.all(relevant_status, dim=1)
@@ -878,32 +868,20 @@ def to_snapshot(self):
878868
Get a snapshot of partition status information.
879869
880870
Returns:
881-
DataPartitionStatus object without threading.Lock()
871+
DataPartitionStatus object
882872
"""
883873

884-
def _perform_copy():
885-
cls = self.__class__
886-
snapshot = cls.__new__(cls)
887-
888-
for name, value in self.__dict__.items():
889-
if name == "data_status_lock":
890-
continue
891-
892-
if isinstance(value, Tensor):
893-
new_val = value.clone().detach()
894-
else:
895-
new_val = copy.deepcopy(value)
874+
cls = self.__class__
875+
snapshot = cls.__new__(cls)
896876

897-
setattr(snapshot, name, new_val)
898-
return snapshot
899-
900-
lock_obj = getattr(self, "data_status_lock", None)
877+
for name, value in self.__dict__.items():
878+
if isinstance(value, Tensor):
879+
new_val = value.clone().detach()
880+
else:
881+
new_val = copy.deepcopy(value)
901882

902-
if lock_obj:
903-
with lock_obj:
904-
return _perform_copy()
905-
else:
906-
return _perform_copy()
883+
setattr(snapshot, name, new_val)
884+
return snapshot
907885

908886
def clear_data(self, indexes_to_release: list[int], clear_consumption: bool = True):
909887
"""Clear all production and optionally consumption data for given global_indexes."""
@@ -1021,7 +999,6 @@ def __init__(
1021999

10221000
# Start background processing threads
10231001
self._start_process_handshake()
1024-
self._start_process_update_data_status()
10251002
self._start_process_request()
10261003

10271004
logger.info(f"TransferQueue Controller {self.controller_id} initialized")
@@ -1070,7 +1047,7 @@ def _get_partition(self, partition_id: str) -> DataPartitionStatus | None:
10701047

10711048
def get_partition_snapshot(self, partition_id: str) -> DataPartitionStatus | None:
10721049
"""
1073-
Get a copy of partition status information, without threading.Lock().
1050+
Get a copy of partition status information.
10741051
10751052
Args:
10761053
partition_id: ID of the partition to retrieve
@@ -1623,8 +1600,7 @@ def kv_retrieve_meta(
16231600
partition.keys_mapping[keys[none_indexes[i]]] = batch_global_indexes[i]
16241601
partition.revert_keys_mapping[batch_global_indexes[i]] = keys[none_indexes[i]]
16251602

1626-
with partition.data_status_lock:
1627-
partition.ensure_samples_capacity(max(batch_global_indexes) + 1)
1603+
partition.ensure_samples_capacity(max(batch_global_indexes) + 1)
16281604

16291605
verified_global_indexes = [idx for idx in global_indexes if idx is not None]
16301606
assert len(verified_global_indexes) == len(keys)
@@ -1685,7 +1661,6 @@ def _init_zmq_socket(self):
16851661
try:
16861662
self._handshake_socket_port = get_free_port(ip=self._node_ip)
16871663
self._request_handle_socket_port = get_free_port(ip=self._node_ip)
1688-
self._data_status_update_socket_port = get_free_port(ip=self._node_ip)
16891664

16901665
self.handshake_socket = create_zmq_socket(
16911666
ctx=self.zmq_context,
@@ -1701,15 +1676,6 @@ def _init_zmq_socket(self):
17011676
)
17021677
self.request_handle_socket.bind(format_zmq_address(self._node_ip, self._request_handle_socket_port))
17031678

1704-
self.data_status_update_socket = create_zmq_socket(
1705-
ctx=self.zmq_context,
1706-
socket_type=zmq.ROUTER,
1707-
ip=self._node_ip,
1708-
)
1709-
self.data_status_update_socket.bind(
1710-
format_zmq_address(self._node_ip, self._data_status_update_socket_port)
1711-
)
1712-
17131679
break
17141680
except zmq.ZMQError:
17151681
logger.warning(f"[{self.controller_id}]: Try to bind ZMQ sockets failed, retrying...")
@@ -1722,7 +1688,6 @@ def _init_zmq_socket(self):
17221688
ports={
17231689
"handshake_socket": self._handshake_socket_port,
17241690
"request_handle_socket": self._request_handle_socket_port,
1725-
"data_status_update_socket": self._data_status_update_socket_port,
17261691
},
17271692
)
17281693

@@ -1781,15 +1746,6 @@ def _start_process_handshake(self):
17811746
)
17821747
self.wait_connection_thread.start()
17831748

1784-
def _start_process_update_data_status(self):
1785-
"""Start the data status update processing thread."""
1786-
self.process_update_data_status_thread = Thread(
1787-
target=self._update_data_status,
1788-
name="TransferQueueControllerProcessUpdateDataStatusThread",
1789-
daemon=True,
1790-
)
1791-
self.process_update_data_status_thread.start()
1792-
17931749
def _start_process_request(self):
17941750
"""Start the request processing thread."""
17951751
self.process_request_thread = Thread(
@@ -1834,6 +1790,36 @@ def _process_request(self):
18341790
body={"metadata": metadata},
18351791
)
18361792

1793+
elif request_msg.request_type == ZMQRequestType.NOTIFY_DATA_UPDATE:
1794+
with monitor.measure(op_type="NOTIFY_DATA_UPDATE"):
1795+
message_data = request_msg.body
1796+
partition_id = message_data.get("partition_id")
1797+
global_indexes = message_data.get("global_indexes", [])
1798+
1799+
# Update production status
1800+
success = self.update_production_status(
1801+
partition_id=partition_id,
1802+
global_indexes=global_indexes,
1803+
field_schema=message_data.get("field_schema", {}),
1804+
custom_backend_meta=message_data.get("custom_backend_meta", {}),
1805+
)
1806+
if success:
1807+
if self._metrics is not None:
1808+
self._metrics.record_samples("NOTIFY_DATA_UPDATE", len(global_indexes))
1809+
logger.debug(f"[{self.controller_id}]: Updated production status for partition {partition_id}")
1810+
1811+
# Send acknowledgment
1812+
response_msg = ZMQMessage.create(
1813+
request_type=ZMQRequestType.NOTIFY_DATA_UPDATE_ACK,
1814+
sender_id=self.controller_id,
1815+
receiver_id=request_msg.sender_id,
1816+
body={
1817+
"controller_id": self.controller_id,
1818+
"partition_id": partition_id,
1819+
"success": success,
1820+
},
1821+
)
1822+
18371823
elif request_msg.request_type == ZMQRequestType.GET_PARTITION_META:
18381824
with monitor.measure(op_type="GET_PARTITION_META"):
18391825
params = request_msg.body
@@ -2047,50 +2033,6 @@ def _process_request(self):
20472033

20482034
self.request_handle_socket.send_multipart([identity, *response_msg.serialize()])
20492035

2050-
def _update_data_status(self):
2051-
"""Process data status update messages from storage units - adapted for partitions."""
2052-
logger.debug(f"[{self.controller_id}]: start receiving update_data_status requests...")
2053-
2054-
perf_monitor = IntervalPerfMonitor(caller_name=self.controller_id)
2055-
2056-
while True:
2057-
monitor = self._metrics if self._metrics is not None else perf_monitor
2058-
2059-
messages = self.data_status_update_socket.recv_multipart(copy=False)
2060-
identity = messages.pop(0)
2061-
serialized_msg = messages
2062-
request_msg = ZMQMessage.deserialize(serialized_msg)
2063-
2064-
if request_msg.request_type == ZMQRequestType.NOTIFY_DATA_UPDATE:
2065-
with monitor.measure(op_type="NOTIFY_DATA_UPDATE"):
2066-
message_data = request_msg.body
2067-
partition_id = message_data.get("partition_id")
2068-
global_indexes = message_data.get("global_indexes", [])
2069-
2070-
# Update production status
2071-
success = self.update_production_status(
2072-
partition_id=partition_id,
2073-
global_indexes=global_indexes,
2074-
field_schema=message_data.get("field_schema", {}),
2075-
custom_backend_meta=message_data.get("custom_backend_meta", {}),
2076-
)
2077-
if success:
2078-
if self._metrics is not None:
2079-
self._metrics.record_samples("NOTIFY_DATA_UPDATE", len(global_indexes))
2080-
logger.debug(f"[{self.controller_id}]: Updated production status for partition {partition_id}")
2081-
2082-
# Send acknowledgment
2083-
response_msg = ZMQMessage.create(
2084-
request_type=ZMQRequestType.NOTIFY_DATA_UPDATE_ACK,
2085-
sender_id=self.controller_id,
2086-
body={
2087-
"controller_id": self.controller_id,
2088-
"partition_id": partition_id,
2089-
"success": success,
2090-
},
2091-
)
2092-
self.data_status_update_socket.send_multipart([identity, *response_msg.serialize()])
2093-
20942036
def get_zmq_server_info(self) -> ZMQServerInfo:
20952037
"""Get ZMQ server connection information."""
20962038
return self.zmq_server_info

transfer_queue/storage/managers/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ async def notify_data_update(
210210
sock = create_zmq_socket(self.zmq_context, zmq.DEALER, self.controller_info.ip, identity)
211211

212212
try:
213-
sock.connect(self.controller_info.to_addr("data_status_update_socket"))
213+
sock.connect(self.controller_info.to_addr("request_handle_socket"))
214214

215215
normalized_field_schema = {}
216216
for field_name, field in field_schema.items():

0 commit comments

Comments
 (0)