751 lines
31 KiB
Python
751 lines
31 KiB
Python
import dis
|
|
import hashlib
|
|
import importlib
|
|
import inspect
|
|
import json
|
|
import logging
|
|
import os
|
|
import sys
|
|
import threading
|
|
import time
|
|
import traceback
|
|
from collections import defaultdict, namedtuple
|
|
from typing import Callable, Optional
|
|
|
|
import ray
|
|
import ray._private.profiling as profiling
|
|
from ray import cloudpickle as pickle
|
|
from ray._common.serialization import pickle_dumps
|
|
from ray._private import ray_constants
|
|
from ray._private.inspect_util import (
|
|
is_class_method,
|
|
is_function_or_method,
|
|
is_static_method,
|
|
)
|
|
from ray._private.ray_constants import KV_NAMESPACE_FUNCTION_TABLE
|
|
from ray._private.utils import (
|
|
check_oversized_function,
|
|
ensure_str,
|
|
format_error_message,
|
|
)
|
|
from ray._raylet import (
|
|
WORKER_PROCESS_SETUP_HOOK_KEY_NAME_GCS,
|
|
JobID,
|
|
PythonFunctionDescriptor,
|
|
)
|
|
from ray.remote_function import RemoteFunction
|
|
from ray.util.tracing.tracing_helper import _inject_tracing_into_class
|
|
|
|
FunctionExecutionInfo = namedtuple(
|
|
"FunctionExecutionInfo", ["function", "function_name", "max_calls"]
|
|
)
|
|
ImportedFunctionInfo = namedtuple(
|
|
"ImportedFunctionInfo",
|
|
["job_id", "function_id", "function_name", "function", "module", "max_calls"],
|
|
)
|
|
|
|
"""FunctionExecutionInfo: A named tuple storing remote function information."""
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def make_function_table_key(key_type: bytes, job_id: JobID, key: Optional[bytes]):
|
|
if key is None:
|
|
return b":".join([key_type, job_id.hex().encode()])
|
|
else:
|
|
return b":".join([key_type, job_id.hex().encode(), key])
|
|
|
|
|
|
def build_setup_hook_export_entry(
|
|
setup_func: Callable, job_id: JobID
|
|
) -> tuple[bytes, bytes, bytes]:
|
|
"""Compute the exported payload and GCS key for a setup hook callable.
|
|
|
|
Args:
|
|
setup_func: The setup hook function to export.
|
|
job_id: The job ID to export the setup hook for.
|
|
|
|
Returns:
|
|
A tuple of (pickled_function, function_id, key).
|
|
"""
|
|
pickled_function = pickle_dumps(
|
|
setup_func,
|
|
"Cannot serialize the worker_process_setup_hook " f"{setup_func.__name__}",
|
|
)
|
|
function_to_run_id = hashlib.shake_128(pickled_function).digest(
|
|
ray_constants.ID_SIZE
|
|
)
|
|
key = make_function_table_key(
|
|
# This value should match with gcs_function_manager.h.
|
|
# Otherwise, it won't be GC'ed.
|
|
WORKER_PROCESS_SETUP_HOOK_KEY_NAME_GCS.encode(),
|
|
# b"FunctionsToRun",
|
|
job_id,
|
|
function_to_run_id,
|
|
)
|
|
return pickled_function, function_to_run_id, key
|
|
|
|
|
|
class FunctionActorManager:
|
|
"""A class used to export/load remote functions and actors.
|
|
Attributes:
|
|
_worker: The associated worker that this manager related.
|
|
_functions_to_export: The remote functions to export when
|
|
the worker gets connected.
|
|
_actors_to_export: The actors to export when the worker gets
|
|
connected.
|
|
_function_execution_info: The function_id
|
|
and execution_info.
|
|
_num_task_executions: The function
|
|
execution times.
|
|
imported_actor_classes: The set of actor classes keys (format:
|
|
ActorClass:function_id) that are already in GCS.
|
|
"""
|
|
|
|
def __init__(self, worker: "ray._private.worker.Worker"):
|
|
"""Initialize FunctionActorManager.
|
|
|
|
Args:
|
|
worker: The worker this manager belongs to.
|
|
"""
|
|
self._worker = worker
|
|
self._functions_to_export = []
|
|
self._actors_to_export = []
|
|
# This field is a dictionary that maps function IDs
|
|
# to a FunctionExecutionInfo object. This should only be used on
|
|
# workers that execute remote functions.
|
|
self._function_execution_info = defaultdict(lambda: {})
|
|
self._num_task_executions = defaultdict(lambda: {})
|
|
# A set of all of the actor class keys that have been imported by the
|
|
# import thread. It is safe to convert this worker into an actor of
|
|
# these types.
|
|
self.imported_actor_classes = set()
|
|
self._loaded_actor_classes = {}
|
|
# Deserialize an ActorHandle will call load_actor_class(). If a
|
|
# function closure captured an ActorHandle, the deserialization of the
|
|
# function will be:
|
|
# -> fetch_and_register_remote_function (acquire lock)
|
|
# -> _load_actor_class_from_gcs (acquire lock, too)
|
|
# So, the lock should be a reentrant lock.
|
|
self.lock = threading.RLock()
|
|
|
|
self.execution_infos = {}
|
|
# This is the counter to keep track of how many keys have already
|
|
# been exported so that we can find next key quicker.
|
|
self._num_exported = 0
|
|
# This is to protect self._num_exported when doing exporting
|
|
self._export_lock = threading.Lock()
|
|
|
|
def increase_task_counter(self, function_descriptor):
|
|
function_id = function_descriptor.function_id
|
|
self._num_task_executions[function_id] += 1
|
|
|
|
def get_task_counter(self, function_descriptor):
|
|
function_id = function_descriptor.function_id
|
|
return self._num_task_executions[function_id]
|
|
|
|
def compute_collision_identifier(self, function_or_class: Callable) -> bytes:
|
|
"""The identifier is used to detect excessive duplicate exports.
|
|
The identifier is used to determine when the same function or class is
|
|
exported many times. This can yield false positives.
|
|
Args:
|
|
function_or_class: The function or class to compute an identifier
|
|
for.
|
|
Returns:
|
|
The identifier. Note that different functions or classes can give
|
|
rise to same identifier. However, the same function should
|
|
hopefully always give rise to the same identifier. TODO(rkn):
|
|
verify if this is actually the case. Note that if the
|
|
identifier is incorrect in any way, then we may give warnings
|
|
unnecessarily or fail to give warnings, but the application's
|
|
behavior won't change.
|
|
"""
|
|
import io
|
|
|
|
string_file = io.StringIO()
|
|
dis.dis(function_or_class, file=string_file, depth=2)
|
|
collision_identifier = function_or_class.__name__ + ":" + string_file.getvalue()
|
|
|
|
# Return a hash of the identifier in case it is too large.
|
|
return hashlib.sha256(collision_identifier.encode("utf-8")).digest()
|
|
|
|
def load_function_or_class_from_local(self, module_name, function_or_class_name):
|
|
"""Try to load a function or class in the module from local."""
|
|
module = importlib.import_module(module_name)
|
|
parts = [part for part in function_or_class_name.split(".") if part]
|
|
object = module
|
|
try:
|
|
for part in parts:
|
|
object = getattr(object, part)
|
|
return object
|
|
except Exception:
|
|
return None
|
|
|
|
def export_setup_func(
|
|
self, setup_func: Callable, timeout: Optional[int] = None
|
|
) -> bytes:
|
|
"""Export the setup hook function and return the key."""
|
|
pickled_function, function_to_run_id, key = build_setup_hook_export_entry(
|
|
setup_func, self._worker.current_job_id.binary()
|
|
)
|
|
|
|
check_oversized_function(
|
|
pickled_function, setup_func.__name__, "function", self._worker
|
|
)
|
|
|
|
try:
|
|
self._worker.gcs_client.internal_kv_put(
|
|
key,
|
|
pickle.dumps(
|
|
{
|
|
"job_id": self._worker.current_job_id.binary(),
|
|
"function_id": function_to_run_id,
|
|
"function": pickled_function,
|
|
}
|
|
),
|
|
# overwrite
|
|
True,
|
|
ray_constants.KV_NAMESPACE_FUNCTION_TABLE,
|
|
timeout=timeout,
|
|
)
|
|
except Exception as e:
|
|
logger.exception(
|
|
"Failed to export the setup hook " f"{setup_func.__name__}."
|
|
)
|
|
raise e
|
|
|
|
return key
|
|
|
|
def export(self, remote_function: RemoteFunction) -> None:
|
|
"""Pickle a remote function and export it to redis.
|
|
|
|
Args:
|
|
remote_function: the RemoteFunction object.
|
|
"""
|
|
if self._worker.load_code_from_local:
|
|
function_descriptor = remote_function._function_descriptor
|
|
module_name, function_name = (
|
|
function_descriptor.module_name,
|
|
function_descriptor.function_name,
|
|
)
|
|
# If the function is dynamic, we still export it to GCS
|
|
# even if load_code_from_local is set True.
|
|
if (
|
|
self.load_function_or_class_from_local(module_name, function_name)
|
|
is not None
|
|
):
|
|
return
|
|
function = remote_function._function
|
|
pickled_function = remote_function._pickled_function
|
|
|
|
check_oversized_function(
|
|
pickled_function,
|
|
remote_function._function_name,
|
|
"remote function",
|
|
self._worker,
|
|
)
|
|
key = make_function_table_key(
|
|
b"RemoteFunction",
|
|
self._worker.current_job_id,
|
|
remote_function._function_descriptor.function_id.binary(),
|
|
)
|
|
if self._worker.gcs_client.internal_kv_exists(key, KV_NAMESPACE_FUNCTION_TABLE):
|
|
return
|
|
val = pickle.dumps(
|
|
{
|
|
"job_id": self._worker.current_job_id.binary(),
|
|
"function_id": remote_function._function_descriptor.function_id.binary(), # noqa: E501
|
|
"function_name": remote_function._function_name,
|
|
"module": function.__module__,
|
|
"function": pickled_function,
|
|
"collision_identifier": self.compute_collision_identifier(function),
|
|
"max_calls": remote_function._max_calls,
|
|
}
|
|
)
|
|
self._worker.gcs_client.internal_kv_put(
|
|
key, val, True, KV_NAMESPACE_FUNCTION_TABLE
|
|
)
|
|
|
|
def fetch_registered_method(
|
|
self, key: str, timeout: Optional[int] = None
|
|
) -> Optional[ImportedFunctionInfo]:
|
|
vals = self._worker.gcs_client.internal_kv_get(
|
|
key, KV_NAMESPACE_FUNCTION_TABLE, timeout=timeout
|
|
)
|
|
if vals is None:
|
|
return None
|
|
else:
|
|
vals = pickle.loads(vals)
|
|
fields = [
|
|
"job_id",
|
|
"function_id",
|
|
"function_name",
|
|
"function",
|
|
"module",
|
|
"max_calls",
|
|
]
|
|
return ImportedFunctionInfo._make(vals.get(field) for field in fields)
|
|
|
|
def fetch_and_register_remote_function(self, key):
|
|
"""Import a remote function."""
|
|
remote_function_info = self.fetch_registered_method(key)
|
|
if not remote_function_info:
|
|
return False
|
|
(
|
|
job_id_str,
|
|
function_id_str,
|
|
function_name,
|
|
serialized_function,
|
|
module,
|
|
max_calls,
|
|
) = remote_function_info
|
|
|
|
function_id = ray.FunctionID(function_id_str)
|
|
job_id = ray.JobID(job_id_str)
|
|
max_calls = int(max_calls)
|
|
|
|
# This function is called by ImportThread. This operation needs to be
|
|
# atomic. Otherwise, there is race condition. Another thread may use
|
|
# the temporary function above before the real function is ready.
|
|
with self.lock:
|
|
self._num_task_executions[function_id] = 0
|
|
|
|
try:
|
|
function = pickle.loads(serialized_function)
|
|
except Exception:
|
|
# If an exception was thrown when the remote function was
|
|
# imported, we record the traceback and notify the scheduler
|
|
# of the failure.
|
|
traceback_str = format_error_message(traceback.format_exc())
|
|
|
|
def f(*args, **kwargs):
|
|
raise RuntimeError(
|
|
"The remote function failed to import on the "
|
|
"worker. This may be because needed library "
|
|
"dependencies are not installed in the worker "
|
|
"environment or cannot be found from sys.path "
|
|
f"{sys.path}:\n\n{traceback_str}"
|
|
)
|
|
|
|
# Use a placeholder method when function pickled failed
|
|
self._function_execution_info[function_id] = FunctionExecutionInfo(
|
|
function=f, function_name=function_name, max_calls=max_calls
|
|
)
|
|
|
|
# Log the error message. Log at DEBUG level to avoid overly
|
|
# spamming the log on import failure. The user gets the error
|
|
# via the RuntimeError message above.
|
|
logger.debug(
|
|
"Failed to unpickle the remote function "
|
|
f"'{function_name}' with "
|
|
f"function ID {function_id.hex()}. "
|
|
f"Job ID:{job_id}."
|
|
f"Traceback:\n{traceback_str}. "
|
|
)
|
|
else:
|
|
# The below line is necessary. Because in the driver process,
|
|
# if the function is defined in the file where the python
|
|
# script was started from, its module is `__main__`.
|
|
# However in the worker process, the `__main__` module is a
|
|
# different module, which is `default_worker.py`
|
|
function.__module__ = module
|
|
self._function_execution_info[function_id] = FunctionExecutionInfo(
|
|
function=function, function_name=function_name, max_calls=max_calls
|
|
)
|
|
return True
|
|
|
|
def get_execution_info(
|
|
self, job_id: JobID, function_descriptor: PythonFunctionDescriptor
|
|
) -> FunctionExecutionInfo:
|
|
"""Get the FunctionExecutionInfo of a remote function.
|
|
Args:
|
|
job_id: ID of the job that the function belongs to.
|
|
function_descriptor: The FunctionDescriptor of the function to get.
|
|
Returns:
|
|
A FunctionExecutionInfo object.
|
|
"""
|
|
function_id = function_descriptor.function_id
|
|
# If the function has already been loaded,
|
|
# There's no need to load again
|
|
if function_id in self._function_execution_info:
|
|
return self._function_execution_info[function_id]
|
|
if self._worker.load_code_from_local:
|
|
# Load function from local code.
|
|
if not function_descriptor.is_actor_method():
|
|
# If the function is not able to be loaded,
|
|
# try to load it from GCS,
|
|
# even if load_code_from_local is set True
|
|
if self._load_function_from_local(function_descriptor) is True:
|
|
return self._function_execution_info[function_id]
|
|
# Load function from GCS.
|
|
# Wait until the function to be executed has actually been
|
|
# registered on this worker. We will push warnings to the user if
|
|
# we spend too long in this loop.
|
|
# The driver function may not be found in sys.path. Try to load
|
|
# the function from GCS.
|
|
with profiling.profile("wait_for_function"):
|
|
self._wait_for_function(function_descriptor, job_id)
|
|
try:
|
|
function_id = function_descriptor.function_id
|
|
info = self._function_execution_info[function_id]
|
|
except KeyError as e:
|
|
message = (
|
|
"Error occurs in get_execution_info: "
|
|
"job_id: %s, function_descriptor: %s. Message: %s"
|
|
% (job_id, function_descriptor, e)
|
|
)
|
|
raise KeyError(message)
|
|
return info
|
|
|
|
def _load_function_from_local(self, function_descriptor):
|
|
assert not function_descriptor.is_actor_method()
|
|
function_id = function_descriptor.function_id
|
|
|
|
module_name, function_name = (
|
|
function_descriptor.module_name,
|
|
function_descriptor.function_name,
|
|
)
|
|
|
|
object = self.load_function_or_class_from_local(module_name, function_name)
|
|
if object is not None:
|
|
# Directly importing from local may break function with dynamic ray.remote,
|
|
# such as the _start_controller function utilized for the Ray service.
|
|
if isinstance(object, RemoteFunction):
|
|
function = object._function
|
|
else:
|
|
function = object
|
|
self._function_execution_info[function_id] = FunctionExecutionInfo(
|
|
function=function,
|
|
function_name=function_name,
|
|
max_calls=0,
|
|
)
|
|
self._num_task_executions[function_id] = 0
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def _wait_for_function(
|
|
self,
|
|
function_descriptor: PythonFunctionDescriptor,
|
|
job_id: str,
|
|
timeout: float = 10,
|
|
):
|
|
"""Wait until the function to be executed is present on this worker.
|
|
This method will simply loop until the import thread has imported the
|
|
relevant function. If we spend too long in this loop, that may indicate
|
|
a problem somewhere and we will push an error message to the user.
|
|
If this worker is an actor, then this will wait until the actor has
|
|
been defined.
|
|
|
|
Args:
|
|
function_descriptor: The FunctionDescriptor of the function that
|
|
we want to execute.
|
|
job_id: The ID of the job to push the error message to
|
|
if this times out.
|
|
timeout: Seconds to wait before pushing a warning to the user.
|
|
"""
|
|
start_time = time.time()
|
|
# Only send the warning once.
|
|
warning_sent = False
|
|
while True:
|
|
with self.lock:
|
|
if self._worker.actor_id.is_nil():
|
|
if function_descriptor.function_id in self._function_execution_info:
|
|
break
|
|
else:
|
|
key = make_function_table_key(
|
|
b"RemoteFunction",
|
|
job_id,
|
|
function_descriptor.function_id.binary(),
|
|
)
|
|
if self.fetch_and_register_remote_function(key) is True:
|
|
break
|
|
else:
|
|
assert not self._worker.actor_id.is_nil()
|
|
# Actor loading will happen when execute_task is called.
|
|
assert self._worker.actor_id in self._worker.actors
|
|
break
|
|
|
|
if time.time() - start_time > timeout:
|
|
warning_message = (
|
|
"This worker was asked to execute a function "
|
|
f"that has not been registered ({function_descriptor}, "
|
|
f"node={self._worker.node_ip_address}, "
|
|
f"worker_id={self._worker.worker_id.hex()}, "
|
|
f"pid={os.getpid()}). You may have to restart Ray."
|
|
)
|
|
if not warning_sent:
|
|
logger.error(warning_message)
|
|
ray._private.utils.push_error_to_driver(
|
|
self._worker,
|
|
ray_constants.WAIT_FOR_FUNCTION_PUSH_ERROR,
|
|
warning_message,
|
|
job_id=job_id,
|
|
)
|
|
warning_sent = True
|
|
time.sleep(0.001)
|
|
|
|
def export_actor_class(
|
|
self, Class, actor_creation_function_descriptor, actor_method_names
|
|
):
|
|
if self._worker.load_code_from_local:
|
|
module_name, class_name = (
|
|
actor_creation_function_descriptor.module_name,
|
|
actor_creation_function_descriptor.class_name,
|
|
)
|
|
# If the class is dynamic, we still export it to GCS
|
|
# even if load_code_from_local is set True.
|
|
if (
|
|
self.load_function_or_class_from_local(module_name, class_name)
|
|
is not None
|
|
):
|
|
return
|
|
|
|
# `current_job_id` shouldn't be NIL, unless:
|
|
# 1) This worker isn't an actor;
|
|
# 2) And a previous task started a background thread, which didn't
|
|
# finish before the task finished, and still uses Ray API
|
|
# after that.
|
|
assert not self._worker.current_job_id.is_nil(), (
|
|
"You might have started a background thread in a non-actor "
|
|
"task, please make sure the thread finishes before the "
|
|
"task finishes."
|
|
)
|
|
job_id = self._worker.current_job_id
|
|
key = make_function_table_key(
|
|
b"ActorClass",
|
|
job_id,
|
|
actor_creation_function_descriptor.function_id.binary(),
|
|
)
|
|
serialized_actor_class = pickle_dumps(
|
|
Class,
|
|
f"Could not serialize the actor class "
|
|
f"{actor_creation_function_descriptor.repr}",
|
|
)
|
|
actor_class_info = {
|
|
"class_name": actor_creation_function_descriptor.class_name.split(".")[-1],
|
|
"module": actor_creation_function_descriptor.module_name,
|
|
"class": serialized_actor_class,
|
|
"job_id": job_id.binary(),
|
|
"collision_identifier": self.compute_collision_identifier(Class),
|
|
"actor_method_names": json.dumps(list(actor_method_names)),
|
|
}
|
|
|
|
check_oversized_function(
|
|
actor_class_info["class"],
|
|
actor_class_info["class_name"],
|
|
"actor",
|
|
self._worker,
|
|
)
|
|
|
|
self._worker.gcs_client.internal_kv_put(
|
|
key, pickle.dumps(actor_class_info), True, KV_NAMESPACE_FUNCTION_TABLE
|
|
)
|
|
# TODO(rkn): Currently we allow actor classes to be defined
|
|
# within tasks. I tried to disable this, but it may be necessary
|
|
# because of https://github.com/ray-project/ray/issues/1146.
|
|
|
|
def load_actor_class(
|
|
self,
|
|
job_id: JobID,
|
|
actor_creation_function_descriptor: PythonFunctionDescriptor,
|
|
) -> type:
|
|
"""Load the actor class.
|
|
Args:
|
|
job_id: job ID of the actor.
|
|
actor_creation_function_descriptor: Function descriptor of
|
|
the actor constructor.
|
|
Returns:
|
|
The actor class.
|
|
"""
|
|
function_id = actor_creation_function_descriptor.function_id
|
|
# Check if the actor class already exists in the cache.
|
|
actor_class = self._loaded_actor_classes.get(function_id, None)
|
|
if actor_class is None:
|
|
# Load actor class.
|
|
if self._worker.load_code_from_local:
|
|
# Load actor class from local code first.
|
|
actor_class = self._load_actor_class_from_local(
|
|
actor_creation_function_descriptor
|
|
)
|
|
# If the actor is unable to be loaded
|
|
# from local, try to load it
|
|
# from GCS even if load_code_from_local is set True
|
|
if actor_class is None:
|
|
actor_class = self._load_actor_class_from_gcs(
|
|
job_id, actor_creation_function_descriptor
|
|
)
|
|
|
|
else:
|
|
# Load actor class from GCS.
|
|
actor_class = self._load_actor_class_from_gcs(
|
|
job_id, actor_creation_function_descriptor
|
|
)
|
|
|
|
# Re-inject tracing into the loaded class. This is necessary because
|
|
# cloudpickle doesn't preserve __signature__ attributes on module-level
|
|
# functions. When a class is pickled and unpickled, user-defined methods
|
|
# are looked up from the module, losing the __signature__ that was set by
|
|
# _inject_tracing_into_class during actor creation. Re-injecting tracing
|
|
# ensures the method signatures include _ray_trace_ctx when tracing is
|
|
# enabled, matching the behavior expected by _tracing_actor_method_invocation.
|
|
_inject_tracing_into_class(actor_class)
|
|
|
|
# Save the loaded actor class in cache.
|
|
self._loaded_actor_classes[function_id] = actor_class
|
|
|
|
# Generate execution info for the methods of this actor class.
|
|
module_name = actor_creation_function_descriptor.module_name
|
|
actor_class_name = actor_creation_function_descriptor.class_name
|
|
actor_methods = inspect.getmembers(
|
|
actor_class, predicate=is_function_or_method
|
|
)
|
|
for actor_method_name, actor_method in actor_methods:
|
|
# Actor creation function descriptor use a unique function
|
|
# hash to solve actor name conflict. When constructing an
|
|
# actor, the actor creation function descriptor will be the
|
|
# key to find __init__ method execution info. So, here we
|
|
# use actor creation function descriptor as method descriptor
|
|
# for generating __init__ method execution info.
|
|
if actor_method_name == "__init__":
|
|
method_descriptor = actor_creation_function_descriptor
|
|
else:
|
|
method_descriptor = PythonFunctionDescriptor(
|
|
module_name, actor_method_name, actor_class_name
|
|
)
|
|
method_id = method_descriptor.function_id
|
|
executor = self._make_actor_method_executor(
|
|
actor_method_name, actor_method
|
|
)
|
|
self._function_execution_info[method_id] = FunctionExecutionInfo(
|
|
function=executor,
|
|
function_name=actor_method_name,
|
|
max_calls=0,
|
|
)
|
|
self._num_task_executions[method_id] = 0
|
|
self._num_task_executions[function_id] = 0
|
|
return actor_class
|
|
|
|
def _load_actor_class_from_local(self, actor_creation_function_descriptor):
|
|
"""Load actor class from local code."""
|
|
module_name, class_name = (
|
|
actor_creation_function_descriptor.module_name,
|
|
actor_creation_function_descriptor.class_name,
|
|
)
|
|
|
|
object = self.load_function_or_class_from_local(module_name, class_name)
|
|
|
|
if object is not None:
|
|
if isinstance(object, ray.actor.ActorClass):
|
|
return object.__ray_metadata__.modified_class
|
|
else:
|
|
return ray.actor._modify_class(object)
|
|
else:
|
|
return None
|
|
|
|
def _create_fake_actor_class(
|
|
self, actor_class_name, actor_method_names, traceback_str
|
|
):
|
|
class TemporaryActor:
|
|
async def __dummy_method(self):
|
|
"""Dummy method for this fake actor class to work for async actors.
|
|
Without this method, this temporary actor class fails to initialize
|
|
if the original actor class was async."""
|
|
pass
|
|
|
|
def temporary_actor_method(*args, **kwargs):
|
|
raise RuntimeError(
|
|
f"The actor with name {actor_class_name} "
|
|
"failed to import on the worker. This may be because "
|
|
"needed library dependencies are not installed in the "
|
|
f"worker environment:\n\n{traceback_str}"
|
|
)
|
|
|
|
for method in actor_method_names:
|
|
setattr(TemporaryActor, method, temporary_actor_method)
|
|
|
|
return TemporaryActor
|
|
|
|
def _load_actor_class_from_gcs(self, job_id, actor_creation_function_descriptor):
|
|
"""Load actor class from GCS."""
|
|
key = make_function_table_key(
|
|
b"ActorClass",
|
|
job_id,
|
|
actor_creation_function_descriptor.function_id.binary(),
|
|
)
|
|
|
|
# Fetch raw data from GCS.
|
|
vals = self._worker.gcs_client.internal_kv_get(key, KV_NAMESPACE_FUNCTION_TABLE)
|
|
fields = ["job_id", "class_name", "module", "class", "actor_method_names"]
|
|
if vals is None:
|
|
vals = {}
|
|
else:
|
|
vals = pickle.loads(vals)
|
|
(job_id_str, class_name, module, pickled_class, actor_method_names) = (
|
|
vals.get(field) for field in fields
|
|
)
|
|
|
|
class_name = ensure_str(class_name)
|
|
module_name = ensure_str(module)
|
|
job_id = ray.JobID(job_id_str)
|
|
actor_method_names = json.loads(ensure_str(actor_method_names))
|
|
|
|
actor_class = None
|
|
try:
|
|
with self.lock:
|
|
actor_class = pickle.loads(pickled_class)
|
|
except Exception:
|
|
logger.debug("Failed to load actor class %s.", class_name)
|
|
# If an exception was thrown when the actor was imported, we record
|
|
# the traceback and notify the scheduler of the failure.
|
|
traceback_str = format_error_message(traceback.format_exc())
|
|
# The actor class failed to be unpickled, create a fake actor
|
|
# class instead (just to produce error messages and to prevent
|
|
# the driver from hanging).
|
|
actor_class = self._create_fake_actor_class(
|
|
class_name, actor_method_names, traceback_str
|
|
)
|
|
|
|
# The below line is necessary. Because in the driver process,
|
|
# if the function is defined in the file where the python script
|
|
# was started from, its module is `__main__`.
|
|
# However in the worker process, the `__main__` module is a
|
|
# different module, which is `default_worker.py`
|
|
actor_class.__module__ = module_name
|
|
return actor_class
|
|
|
|
def _make_actor_method_executor(self, method_name: str, method: Callable):
|
|
"""Make an executor that wraps a user-defined actor method.
|
|
The wrapped method updates the worker's internal state and performs any
|
|
necessary checkpointing operations.
|
|
Args:
|
|
method_name: The name of the actor method.
|
|
method: The actor method to wrap. This should be a
|
|
method defined on the actor class and should therefore take an
|
|
instance of the actor as the first argument.
|
|
Returns:
|
|
A function that executes the given actor method on the worker's
|
|
stored instance of the actor. The function also updates the
|
|
worker's internal state to record the executed method.
|
|
"""
|
|
|
|
def actor_method_executor(__ray_actor, *args, **kwargs):
|
|
# Execute the assigned method.
|
|
is_bound = is_class_method(method) or is_static_method(
|
|
type(__ray_actor), method_name
|
|
)
|
|
if is_bound:
|
|
return method(*args, **kwargs)
|
|
else:
|
|
return method(__ray_actor, *args, **kwargs)
|
|
|
|
# Set method_name and method as attributes to the executor closure
|
|
# so we can make decision based on these attributes in task executor.
|
|
# Precisely, asyncio support requires to know whether:
|
|
# - the method is a ray internal method: starts with __ray
|
|
# - the method is a coroutine function: defined by async def
|
|
actor_method_executor.name = method_name
|
|
actor_method_executor.method = method
|
|
|
|
return actor_method_executor
|