chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:17:40 +08:00
commit f1825c8ceb
10096 changed files with 2364182 additions and 0 deletions
@@ -0,0 +1,776 @@
import asyncio
import json
import logging
import time
from collections import defaultdict, deque
from concurrent.futures import ThreadPoolExecutor
from itertools import chain
from typing import Any, AsyncGenerator, Dict, Iterable, List, Optional, Set
import aiohttp.web
import grpc
import ray._private.utils
import ray.dashboard.optional_utils as dashboard_optional_utils
import ray.dashboard.utils as dashboard_utils
from ray._common.utils import get_or_create_event_loop
from ray._private import ray_constants
from ray._private.collections_utils import split
from ray._private.gcs_pubsub import (
GcsAioActorSubscriber,
GcsAioNodeInfoSubscriber,
GcsAioResourceUsageSubscriber,
)
from ray._private.grpc_utils import init_grpc_channel
from ray._private.ray_constants import (
DEBUG_AUTOSCALING_ERROR,
DEBUG_AUTOSCALING_STATUS,
env_integer,
)
from ray.autoscaler._private.util import (
LoadMetricsSummary,
get_per_node_breakdown_as_dict,
parse_usage,
)
from ray.core.generated import gcs_pb2, node_manager_pb2, node_manager_pb2_grpc
from ray.dashboard.consts import (
DASHBOARD_AGENT_ADDR_IP_PREFIX,
DASHBOARD_AGENT_ADDR_NODE_ID_PREFIX,
GCS_RPC_TIMEOUT_SECONDS,
)
from ray.dashboard.modules.node import actor_consts, node_consts
from ray.dashboard.modules.node.datacenter import DataOrganizer, DataSource
from ray.dashboard.modules.reporter.reporter_models import StatsPayload
from ray.dashboard.subprocesses.module import SubprocessModule
from ray.dashboard.subprocesses.routes import SubprocessRouteTable as routes
from ray.dashboard.utils import async_loop_forever
logger = logging.getLogger(__name__)
# NOTE: Executor in this head is intentionally constrained to just 1 thread by
# default to limit its concurrency, therefore reducing potential for
# GIL contention
RAY_DASHBOARD_NODE_HEAD_TPE_MAX_WORKERS = env_integer(
"RAY_DASHBOARD_NODE_HEAD_TPE_MAX_WORKERS", 1
)
MAX_DESTROYED_ACTORS_TO_CACHE = max(
0, ray._config.maximum_gcs_destroyed_actor_cached_count()
)
ACTOR_CLEANUP_FREQUENCY = 1 # seconds
ACTOR_TABLE_STATE_COLUMNS = (
"state",
"address",
"numRestarts",
"timestamp",
"pid",
"exitDetail",
"startTime",
"endTime",
"reprName",
)
def _gcs_node_info_to_dict(message: gcs_pb2.GcsNodeInfo) -> dict:
return dashboard_utils.message_to_dict(
message, {"nodeId"}, always_print_fields_with_no_presence=True
)
def _actor_table_data_to_dict(message):
orig_message = dashboard_utils.message_to_dict(
message,
{
"actorId",
"parentId",
"jobId",
"workerId",
"nodeId",
"callerId",
"taskId",
"parentTaskId",
"sourceActorId",
"placementGroupId",
},
always_print_fields_with_no_presence=True,
)
# The complete schema for actor table is here:
# src/ray/protobuf/gcs.proto
# It is super big and for dashboard, we don't need that much information.
# Only preserve the necessary ones here for memory usage.
fields = {
"actorId",
"jobId",
"pid",
"address",
"state",
"name",
"numRestarts",
"timestamp",
"className",
"startTime",
"endTime",
"reprName",
"placementGroupId",
"callSite",
"labelSelector",
"fallbackStrategy",
}
light_message = {k: v for (k, v) in orig_message.items() if k in fields}
light_message["actorClass"] = orig_message["className"]
exit_detail = "-"
if "deathCause" in orig_message:
context = orig_message["deathCause"]
if "actorDiedErrorContext" in context:
exit_detail = context["actorDiedErrorContext"]["errorMessage"] # noqa
elif "runtimeEnvFailedContext" in context:
exit_detail = context["runtimeEnvFailedContext"]["errorMessage"] # noqa
elif "actorUnschedulableContext" in context:
exit_detail = context["actorUnschedulableContext"]["errorMessage"] # noqa
elif "creationTaskFailureContext" in context:
exit_detail = context["creationTaskFailureContext"][
"formattedExceptionString"
] # noqa
light_message["exitDetail"] = exit_detail
light_message["startTime"] = int(light_message["startTime"])
light_message["endTime"] = int(light_message["endTime"])
light_message["requiredResources"] = dict(message.required_resources)
light_message["labelSelector"] = dict(message.label_selector)
return light_message
class NodeHead(SubprocessModule):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._stubs = {}
self._collect_memory_info = False
# The time where the module is started.
self._module_start_time = time.time()
# The time it takes until the head node is registered. None means
# head node hasn't been registered.
self._head_node_registration_time_s = None
# The node ID of the current head node
self._registered_head_node_id = None
# Queue of dead nodes to be removed, up to MAX_DEAD_NODES_TO_CACHE
self._dead_node_queue = deque()
self._node_executor = ThreadPoolExecutor(
max_workers=RAY_DASHBOARD_NODE_HEAD_TPE_MAX_WORKERS,
thread_name_prefix="node_head_node_executor",
)
self._gcs_actor_channel_subscriber = None
# A queue of dead actors in order of when they died
self._destroyed_actors_queue = deque()
# -- Internal state --
self._loop = get_or_create_event_loop()
# NOTE: This executor is intentionally constrained to just 1 thread to
# limit its concurrency, therefore reducing potential for GIL contention
self._actor_executor = ThreadPoolExecutor(
max_workers=1, thread_name_prefix="node_head_actor_executor"
)
self._background_tasks: Set[asyncio.Task] = set()
def get_internal_states(self):
return {
"head_node_registration_time_s": self._head_node_registration_time_s,
"registered_nodes": len(DataSource.nodes),
"module_lifetime_s": time.time() - self._module_start_time,
}
async def _subscribe_for_node_updates(self) -> AsyncGenerator[dict, None]:
"""
Yields the initial state of all nodes, then yields the updated state of nodes.
It makes GetAllNodeInfo call only once after the subscription is done, to get
the initial state of the nodes.
"""
subscriber = GcsAioNodeInfoSubscriber(address=self.gcs_address)
await subscriber.subscribe()
# Get all node info from GCS. To prevent Time-of-check to time-of-use issue [1],
# it happens after the subscription. That is, an update between
# get-all-node-info and the subscription is not missed.
# [1] https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use
node_infos, _ = await self.gcs_client.async_get_all_node_info(timeout=None)
def _convert_to_dict(messages: Iterable[gcs_pb2.GcsNodeInfo]) -> List[dict]:
return [_gcs_node_info_to_dict(m) for m in messages]
all_node_infos = await self._loop.run_in_executor(
self._node_executor,
_convert_to_dict,
node_infos.values(),
)
for node in all_node_infos:
yield node
while True:
try:
node_id_updated_info_tuples = await subscriber.poll(
batch_size=node_consts.RAY_DASHBOARD_NODE_SUBSCRIBER_POLL_SIZE
)
if node_id_updated_info_tuples:
_, updated_infos_proto = zip(*node_id_updated_info_tuples)
else:
updated_infos_proto = []
updated_infos = await self._loop.run_in_executor(
self._node_executor,
_convert_to_dict,
updated_infos_proto,
)
for node in updated_infos:
yield node
except Exception:
logger.exception("Failed handling updated nodes.")
async def _update_node(self, node: dict):
node_id = node["nodeId"]
if (
node["isHeadNode"]
and node["state"] == "ALIVE"
and self._registered_head_node_id != node_id
):
if self._registered_head_node_id is not None:
logger.warning(
"A new head node has become ALIVE. New head node ID: %s, old head node ID: %s, internal states: %s",
node_id,
self._registered_head_node_id,
self.get_internal_states(),
)
self._registered_head_node_id = node_id
self._head_node_registration_time_s = time.time() - self._module_start_time
# Put head node ID in the internal KV to be read by JobAgent.
# TODO(architkulkarni): Remove once State API exposes which
# node is the head node.
await self.gcs_client.async_internal_kv_put(
ray_constants.KV_HEAD_NODE_ID_KEY,
node_id.encode(),
overwrite=True,
namespace=ray_constants.KV_NAMESPACE_JOB,
timeout=GCS_RPC_TIMEOUT_SECONDS,
)
assert node["state"] in ["ALIVE", "DEAD"]
is_alive = node["state"] == "ALIVE"
if not is_alive:
# Remove the agent address from the internal KV.
keys = [
f"{DASHBOARD_AGENT_ADDR_NODE_ID_PREFIX}{node_id}",
f"{DASHBOARD_AGENT_ADDR_IP_PREFIX}{node['nodeManagerAddress']}",
]
tasks = [
self.gcs_client.async_internal_kv_del(
key,
del_by_prefix=False,
namespace=ray_constants.KV_NAMESPACE_DASHBOARD,
timeout=GCS_RPC_TIMEOUT_SECONDS,
)
for key in keys
]
await asyncio.gather(*tasks)
self._dead_node_queue.append(node_id)
if len(self._dead_node_queue) > node_consts.MAX_DEAD_NODES_TO_CACHE:
evicted_node_id = self._dead_node_queue.popleft()
DataSource.nodes.pop(evicted_node_id, None)
self._stubs.pop(evicted_node_id, None)
DataSource.nodes[node_id] = node
# TODO(fyrestone): Handle exceptions.
address = "{}:{}".format(
node["nodeManagerAddress"], int(node["nodeManagerPort"])
)
options = ray_constants.GLOBAL_GRPC_OPTIONS
channel = init_grpc_channel(address, options, asynchronous=True)
stub = node_manager_pb2_grpc.NodeManagerServiceStub(channel)
self._stubs[node_id] = stub
async def _update_nodes(self):
"""
Subscribe to node updates and update the internal states. If the head node is
not registered after RAY_DASHBOARD_HEAD_NODE_REGISTRATION_TIMEOUT, it logs a
warning only once.
"""
warning_shown = False
async for node in self._subscribe_for_node_updates():
await self._update_node(node)
if not self._head_node_registration_time_s:
# head node is not registered yet
if (
not warning_shown
and (time.time() - self._module_start_time)
> node_consts.RAY_DASHBOARD_HEAD_NODE_REGISTRATION_TIMEOUT
):
logger.warning(
"Head node is not registered even after "
f"{node_consts.RAY_DASHBOARD_HEAD_NODE_REGISTRATION_TIMEOUT} seconds. "
"The API server might not work correctly. Please "
"report a Github issue. Internal states :"
f"{self.get_internal_states()}"
)
warning_shown = True
async def get_nodes_logical_resources(self) -> dict:
from ray.autoscaler.v2.utils import is_autoscaler_v2
if is_autoscaler_v2():
from ray.autoscaler.v2.schema import Stats
from ray.autoscaler.v2.sdk import ClusterStatusParser
try:
# here we have a sync request
req_time = time.time()
cluster_status = await self.gcs_client.async_get_cluster_status()
reply_time = time.time()
cluster_status = ClusterStatusParser.from_get_cluster_status_reply(
cluster_status,
stats=Stats(
gcs_request_time_s=reply_time - req_time, request_ts_s=req_time
),
)
except Exception:
logger.exception("Error getting cluster status")
return {}
per_node_resources = {}
# TODO(rickyx): we should just return structure data rather than strings.
for node in chain(cluster_status.active_nodes, cluster_status.idle_nodes):
if not node.resource_usage:
continue
usage_dict = {
r.resource_name: (r.used, r.total)
for r in node.resource_usage.usage
}
per_node_resources[node.node_id] = "\n".join(
parse_usage(usage_dict, verbose=True)
)
return per_node_resources
# Legacy autoscaler status code.
(status_string, error) = await asyncio.gather(
*[
self.gcs_client.async_internal_kv_get(
key.encode(), namespace=None, timeout=GCS_RPC_TIMEOUT_SECONDS
)
for key in [
DEBUG_AUTOSCALING_STATUS,
DEBUG_AUTOSCALING_ERROR,
]
]
)
if not status_string:
return {}
status_dict = json.loads(status_string)
lm_summary_dict = status_dict.get("load_metrics_report")
if lm_summary_dict:
lm_summary = LoadMetricsSummary(**lm_summary_dict)
node_logical_resources = get_per_node_breakdown_as_dict(lm_summary)
return node_logical_resources if error is None else {}
@routes.get("/nodes")
@dashboard_optional_utils.aiohttp_cache
async def get_all_nodes(self, req) -> aiohttp.web.Response:
view = req.query.get("view")
if view == "summary":
all_node_summary_task = DataOrganizer.get_all_node_summary()
nodes_logical_resource_task = self.get_nodes_logical_resources()
all_node_summary, nodes_logical_resources = await asyncio.gather(
all_node_summary_task, nodes_logical_resource_task
)
return dashboard_optional_utils.rest_response(
status_code=dashboard_utils.HTTPStatusCode.OK,
message="Node summary fetched.",
summary=all_node_summary,
node_logical_resources=nodes_logical_resources,
)
elif view is not None and view.lower() == "hostNameList".lower():
alive_hostnames = set()
for node in DataSource.nodes.values():
if node["state"] == "ALIVE":
alive_hostnames.add(node["nodeManagerHostname"])
return dashboard_optional_utils.rest_response(
status_code=dashboard_utils.HTTPStatusCode.OK,
message="Node hostname list fetched.",
host_name_list=list(alive_hostnames),
)
else:
return dashboard_optional_utils.rest_response(
status_code=dashboard_utils.HTTPStatusCode.INTERNAL_ERROR,
message=f"Unknown view {view}",
)
@routes.get("/nodes/{node_id}")
@dashboard_optional_utils.aiohttp_cache
async def get_node(self, req) -> aiohttp.web.Response:
node_id = req.match_info.get("node_id")
node_info = await DataOrganizer.get_node_info(node_id)
return dashboard_optional_utils.rest_response(
status_code=dashboard_utils.HTTPStatusCode.OK,
message="Node details fetched.",
detail=node_info,
)
@async_loop_forever(node_consts.NODE_STATS_UPDATE_INTERVAL_SECONDS)
async def _update_node_stats(self):
timeout = max(2, node_consts.NODE_STATS_UPDATE_INTERVAL_SECONDS - 1)
# NOTE: We copy stubs to make sure
# it doesn't change during the iteration (since its being updated
# from another async task)
current_stub_node_id_tuples = list(self._stubs.items())
node_ids = []
get_node_stats_tasks = []
for _, (node_id, stub) in enumerate(current_stub_node_id_tuples):
node_info = DataSource.nodes.get(node_id)
if node_info["state"] != "ALIVE":
continue
node_ids.append(node_id)
get_node_stats_tasks.append(
stub.GetNodeStats(
node_manager_pb2.GetNodeStatsRequest(
include_memory_info=self._collect_memory_info
),
timeout=timeout,
)
)
responses = []
# NOTE: We're chunking up fetching of the stats to run in batches of no more
# than 100 nodes at a time to avoid flooding the event-loop's queue
# with potentially a large, uninterrupted sequence of tasks updating
# the node stats for very large clusters.
for get_node_stats_tasks_chunk in split(get_node_stats_tasks, 100):
current_chunk_responses = await asyncio.gather(
*get_node_stats_tasks_chunk,
return_exceptions=True,
)
responses.extend(current_chunk_responses)
# We're doing short (25ms) yield after every chunk to make sure
# - We're not overloading the event-loop with excessive # of tasks
# - Allowing 10k nodes stats fetches be sent out performed in 2.5s
await asyncio.sleep(0.025)
def postprocess(node_id_response_tuples):
"""Pure function reorganizing the data into {node_id: stats}."""
new_node_stats = {}
for node_id, response in node_id_response_tuples:
if isinstance(response, asyncio.CancelledError):
pass
elif isinstance(response, grpc.RpcError):
if response.code() == grpc.StatusCode.DEADLINE_EXCEEDED:
message = (
f"Cannot reach the node, {node_id}, after timeout "
f" {timeout}. This node may have been overloaded, "
"terminated, or the network is slow."
)
elif response.code() == grpc.StatusCode.UNAVAILABLE:
message = (
f"Cannot reach the node, {node_id}. "
"The node may have been terminated."
)
else:
message = f"Error updating node stats of {node_id}."
logger.error(message, exc_info=response)
elif isinstance(response, Exception):
logger.error(
f"Error updating node stats of {node_id}.", exc_info=response
)
else:
new_node_stats[node_id] = dashboard_utils.node_stats_to_dict(
response
)
return new_node_stats
# NOTE: Zip will silently truncate to shorter argument that potentially
# could lead to subtle hard to catch issues, hence the assertion
assert len(node_ids) == len(
responses
), f"node_ids({len(node_ids)}): {node_ids}, responses({len(responses)}): {responses}"
new_node_stats = await self._loop.run_in_executor(
self._node_executor, postprocess, zip(node_ids, responses)
)
for node_id, new_stat in new_node_stats.items():
DataSource.node_stats[node_id] = new_stat
async def _update_node_physical_stats(self):
"""
Update DataSource.node_physical_stats by subscribing to the GCS resource usage.
"""
subscriber = GcsAioResourceUsageSubscriber(address=self.gcs_address)
await subscriber.subscribe()
while True:
try:
# The key is b'RAY_REPORTER:{node id hex}',
# e.g. b'RAY_REPORTER:2b4fbd...'
key, data = await subscriber.poll()
if key is None:
continue
# NOTE: Every iteration is executed inside the thread-pool executor
# (TPE) to avoid blocking the Dashboard's event-loop
parsed_data = await self._loop.run_in_executor(
self._node_executor, _parse_node_stats, data
)
node_id = key.split(":")[-1]
DataSource.node_physical_stats[node_id] = parsed_data
except Exception:
logger.exception(
"Error receiving node physical stats from _update_node_physical_stats."
)
async def _update_actors(self):
"""
Processes actor info. First gets all actors from GCS, then subscribes to
actor updates. For each actor update, updates DataSource.node_actors and
DataSource.actors.
"""
# To prevent Time-of-check to time-of-use issue [1], the get-all-actor-info
# happens after the subscription. That is, an update between get-all-actor-info
# and the subscription is not missed.
#
# [1] https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use
gcs_addr = self.gcs_address
actor_channel_subscriber = GcsAioActorSubscriber(address=gcs_addr)
await actor_channel_subscriber.subscribe()
# Get all actor info.
while True:
try:
logger.info("Getting all actor info from GCS.")
actor_dicts = await self._get_all_actors()
# Update actors
DataSource.actors = actor_dicts
# Update node actors and job actors.
node_actors = defaultdict(dict)
for actor_id_bytes, updated_actor_table in actor_dicts.items():
node_id = updated_actor_table["address"]["nodeId"]
# Update only when node_id is not Nil.
if node_id != actor_consts.NIL_NODE_ID:
node_actors[node_id][actor_id_bytes] = updated_actor_table
# Update node's actor info
DataSource.node_actors = node_actors
logger.info("Received %d actor info from GCS.", len(actor_dicts))
# Break, once all initial actors are successfully fetched
break
except Exception as e:
logger.exception("Error Getting all actor info from GCS", exc_info=e)
await asyncio.sleep(
actor_consts.RETRY_GET_ALL_ACTOR_INFO_INTERVAL_SECONDS
)
# Pull incremental updates from the GCS channel
while True:
try:
updated_actor_table_entries = await self._poll_updated_actor_table_data(
actor_channel_subscriber
)
for (
actor_id,
updated_actor_table,
) in updated_actor_table_entries.items():
self._process_updated_actor_table(actor_id, updated_actor_table)
# TODO emit metrics
logger.debug(
f"Total events processed: {len(updated_actor_table_entries)}, "
f"queue size: {actor_channel_subscriber.queue_size}"
)
except Exception as e:
logger.exception("Error processing actor info from GCS.", exc_info=e)
async def _poll_updated_actor_table_data(
self, actor_channel_subscriber: GcsAioActorSubscriber
) -> Dict[str, Dict[str, Any]]:
# TODO make batch size configurable
batch = await actor_channel_subscriber.poll(batch_size=200)
# NOTE: We're offloading conversion to a TPE to make sure we're not
# blocking the event-loop for prolonged period of time irrespective
# of the batch size
def _convert_to_dict():
return {
actor_id_bytes.hex(): _actor_table_data_to_dict(
actor_table_data_message
)
for actor_id_bytes, actor_table_data_message in batch
if actor_id_bytes is not None
}
return await self._loop.run_in_executor(self._actor_executor, _convert_to_dict)
def _process_updated_actor_table(
self, actor_id: str, actor_table_data: Dict[str, Any]
):
"""NOTE: This method has to be executed on the event-loop, provided that it
accesses DataSource data structures (to follow its thread-safety model)"""
# If actor is not new registered but updated, we only update
# states related fields.
actor = DataSource.actors.get(actor_id)
if actor and actor_table_data["state"] != "DEPENDENCIES_UNREADY":
for k in ACTOR_TABLE_STATE_COLUMNS:
if k in actor_table_data:
actor[k] = actor_table_data[k]
actor_table_data = actor
actor_id = actor_table_data["actorId"]
node_id = actor_table_data["address"]["nodeId"]
if actor_table_data["state"] == "DEAD":
self._destroyed_actors_queue.append(actor_id)
# Update actors.
DataSource.actors[actor_id] = actor_table_data
# Update node actors (only when node_id is not Nil).
if node_id != actor_consts.NIL_NODE_ID:
node_actors = DataSource.node_actors.get(node_id, {})
node_actors[actor_id] = actor_table_data
DataSource.node_actors[node_id] = node_actors
async def _get_all_actors(self) -> Dict[str, dict]:
actors = await self.gcs_client.async_get_all_actor_info(
timeout=GCS_RPC_TIMEOUT_SECONDS
)
# NOTE: We're offloading conversion to a TPE to make sure we're not
# blocking the event-loop for prolonged period of time for large clusters
def _convert_to_dict():
return {
actor_id.hex(): _actor_table_data_to_dict(actor_table_data)
for actor_id, actor_table_data in actors.items()
}
return await self._loop.run_in_executor(self._actor_executor, _convert_to_dict)
async def _cleanup_actors(self):
while True:
try:
while len(self._destroyed_actors_queue) > MAX_DESTROYED_ACTORS_TO_CACHE:
actor_id = self._destroyed_actors_queue.popleft()
if actor_id in DataSource.actors:
actor = DataSource.actors.pop(actor_id)
node_id = actor["address"].get("nodeId")
if node_id and node_id != actor_consts.NIL_NODE_ID:
del DataSource.node_actors[node_id][actor_id]
await asyncio.sleep(ACTOR_CLEANUP_FREQUENCY)
except Exception:
logger.exception("Error cleaning up actor info from GCS.")
@routes.get("/logical/actors")
@dashboard_optional_utils.aiohttp_cache
async def get_all_actors(self, req) -> aiohttp.web.Response:
actor_ids: Optional[List[str]] = None
if "ids" in req.query:
actor_ids = req.query["ids"].split(",")
actors = await DataOrganizer.get_actor_infos(actor_ids=actor_ids)
return dashboard_optional_utils.rest_response(
status_code=dashboard_utils.HTTPStatusCode.OK,
message="All actors fetched.",
actors=actors,
# False to avoid converting Ray resource name to google style.
# It's not necessary here because the fields are already
# google formatted when protobuf was converted into dict.
convert_google_style=False,
)
@routes.get("/logical/actors/{actor_id}")
@dashboard_optional_utils.aiohttp_cache
async def get_actor(self, req) -> aiohttp.web.Response:
actor_id = req.match_info.get("actor_id")
actors = await DataOrganizer.get_actor_infos(actor_ids=[actor_id])
return dashboard_optional_utils.rest_response(
status_code=dashboard_utils.HTTPStatusCode.OK,
message="Actor details fetched.",
detail=actors[actor_id],
)
@routes.get("/test/dump")
async def dump(self, req) -> aiohttp.web.Response:
"""
Dump all data from datacenter. This is used for testing purpose only.
"""
key = req.query.get("key")
if key is None:
all_data = {
k: dict(v)
for k, v in DataSource.__dict__.items()
if not k.startswith("_")
}
return dashboard_optional_utils.rest_response(
status_code=dashboard_utils.HTTPStatusCode.OK,
message="Fetch all data from datacenter success.",
**all_data,
)
else:
data = dict(DataSource.__dict__.get(key))
return dashboard_optional_utils.rest_response(
status_code=dashboard_utils.HTTPStatusCode.OK,
message=f"Fetch {key} from datacenter success.",
**{key: data},
)
async def run(self):
await super().run()
coros = [
self._update_nodes(),
self._update_node_stats(),
self._update_node_physical_stats(),
self._update_actors(),
self._cleanup_actors(),
DataOrganizer.purge(),
DataOrganizer.organize(self._node_executor),
]
for coro in coros:
task = self._loop.create_task(coro)
self._background_tasks.add(task)
task.add_done_callback(self._background_tasks.discard)
def _parse_node_stats(node_stats_str: str) -> dict:
stats_dict = json.loads(node_stats_str)
if StatsPayload is not None:
# Validate the response by parsing the stats_dict.
StatsPayload.parse_obj(stats_dict)
return stats_dict
else:
return stats_dict