Files
ray-project--ray/python/ray/_raylet.pyx
T
2026-07-13 13:17:40 +08:00

5311 lines
222 KiB
Cython

# cython: profile=False
# distutils: language = c++
# cython: embedsignature = True
# cython: language_level = 3
# cython: c_string_encoding = default
from cpython.exc cimport PyErr_CheckSignals
import asyncio
import gc
import inspect
import logging
import msgpack
import io
import os
import pickle
import random
import sys
import threading
import time
import traceback
import _thread
from typing import (
Any,
AsyncGenerator,
Awaitable,
Callable,
Dict,
Generator,
Optional,
Tuple,
Union,
NamedTuple,
)
import contextvars
import concurrent.futures
import collections
from dataclasses import dataclass
from libc.stdint cimport (
int32_t,
int64_t,
uint64_t,
uint8_t,
)
from libcpp cimport bool as c_bool, nullptr
from libcpp.memory cimport (
dynamic_pointer_cast,
make_shared,
shared_ptr,
make_unique,
unique_ptr,
)
from ray.includes.optional cimport (
optional,
nullopt,
make_optional,
)
from libcpp.functional cimport function
from libcpp.string cimport string as c_string
from libcpp.utility cimport pair
from libcpp.unordered_map cimport unordered_map
from libcpp.vector cimport vector as c_vector
from libcpp.pair cimport pair as c_pair
from cpython.object cimport PyTypeObject
from cython.operator import dereference, postincrement
from cpython.pystate cimport (
PyGILState_Ensure,
PyGILState_Release,
PyGILState_STATE,
)
from ray.includes.common cimport (
CBuffer,
CAddress,
CObjectReference,
CLanguage,
CObjectReference,
CWorkerExitType,
CRayObject,
CRayStatus,
CStatusOr,
CActorTableData,
CErrorTableData,
CFallbackOption,
CGcsClientOptions,
CGcsNodeInfo,
CJobTableData,
CLabelSelector,
CLogBatch,
CTaskArg,
CTaskArgByReference,
CTaskArgByValue,
CTaskType,
CPlacementStrategy,
CSchedulingStrategy,
CPlacementGroupSchedulingStrategy,
CNodeAffinitySchedulingStrategy,
CNodeLabelSchedulingStrategy,
CLabelMatchExpressions,
CLabelMatchExpression,
CLabelIn,
CLabelNotIn,
CLabelSelector,
CNodeResources,
CRayFunction,
CWorkerType,
CJobConfig,
CConcurrencyGroup,
CGrpcStatusCode,
CLineageReconstructionTask,
move,
LANGUAGE_CPP,
LANGUAGE_JAVA,
LANGUAGE_PYTHON,
LocalMemoryBuffer,
TASK_TYPE_NORMAL_TASK,
TASK_TYPE_ACTOR_CREATION_TASK,
TASK_TYPE_ACTOR_TASK,
WORKER_TYPE_WORKER,
WORKER_TYPE_DRIVER,
WORKER_TYPE_SPILL_WORKER,
WORKER_TYPE_RESTORE_WORKER,
PLACEMENT_STRATEGY_PACK,
PLACEMENT_STRATEGY_SPREAD,
PLACEMENT_STRATEGY_STRICT_PACK,
PLACEMENT_STRATEGY_STRICT_SPREAD,
RAY_ERROR_INFO_CHANNEL,
RAY_LOG_CHANNEL,
PythonGetLogBatchLines,
WORKER_EXIT_TYPE_USER_ERROR,
WORKER_EXIT_TYPE_SYSTEM_ERROR,
WORKER_EXIT_TYPE_INTENTIONAL_SYSTEM_ERROR,
kResourceUnitScaling,
kImplicitResourcePrefix,
kWorkerSetupHookKeyName,
PythonGetNodeLabels,
PythonGetResourcesTotal,
kGcsPidKey,
GetPortFileName,
PersistPort,
WaitForPersistedPort,
CWaitForPersistedPortResult,
SetNodeResourcesLabels,
)
from ray.includes.unique_ids cimport (
CActorID,
CClusterID,
CNodeID,
CObjectID,
CPlacementGroupID,
ObjectIDIndexType,
)
from ray.includes.libcoreworker cimport (
ActorHandleSharedPtr,
CActorCreationOptions,
CPlacementGroupCreationOptions,
CCoreWorkerOptions,
CCoreWorkerProcess,
CTaskOptions,
ResourceMappingType,
CFiberEvent,
CTaskGeneratorBackpressureWaiter,
CActorWideGeneratorBackpressureWaiter,
CActorTaskBackpressureMetadata,
CReaderRefInfo,
)
from ray.includes.stream_redirection cimport (
CStreamRedirectionOptions,
RedirectStdoutOncePerProcess,
RedirectStderrOncePerProcess,
)
from ray.includes.ray_config cimport RayConfig
from ray.includes.global_state_accessor cimport CGlobalStateAccessor
from ray.includes.global_state_accessor cimport (
RedisDelKeyPrefixSync,
RedisGetKeySync
)
cimport cpython
include "includes/network_util.pxi"
include "includes/object_ref.pxi"
include "includes/unique_ids.pxi"
include "includes/ray_config.pxi"
include "includes/function_descriptor.pxi"
include "includes/buffer.pxi"
include "includes/common.pxi"
include "includes/gcs_client.pxi"
include "includes/serialization.pxi"
include "includes/libcoreworker.pxi"
include "includes/global_state_accessor.pxi"
include "includes/metric.pxi"
include "includes/event_recorder.pxi"
include "includes/setproctitle.pxi"
include "includes/raylet_client.pxi"
include "includes/gcs_subscriber.pxi"
include "includes/rpc_token_authentication.pxi"
include "includes/task_options_utils.pxi"
# Ray Serve-only: Cython timeseries utilities for autoscaling metrics.
include "includes/timeseries_utils.pxi"
import ray
from ray.exceptions import (
ActorHandleNotFoundError,
ActorDiedError,
RayActorError,
RayError,
RaySystemError,
RayTaskError,
ObjectStoreFullError,
OutOfDiskError,
GetTimeoutError,
TaskCancelledError,
AsyncioActorExit,
PendingCallsLimitExceeded,
RpcError,
ObjectRefStreamEndOfStreamError,
RayChannelError,
RayChannelTimeoutError,
)
from ray._private import external_storage
from ray.util.scheduling_strategies import (
PlacementGroupSchedulingStrategy,
NodeAffinitySchedulingStrategy,
NodeLabelSchedulingStrategy,
In,
NotIn,
Exists,
DoesNotExist,
)
import ray._private.ray_constants as ray_constants
import ray.cloudpickle as ray_pickle
from ray.core.generated.common_pb2 import ActorDiedErrorContext
from ray.core.generated.gcs_service_pb2 import GetAllResourceUsageReply
from ray._private.async_compat import (
sync_to_async,
get_new_event_loop,
is_async_func,
has_async_methods,
)
from ray._private.client_mode_hook import disable_client_hook
import ray.core.generated.common_pb2 as common_pb2
from ray._common.utils import decode
from ray._private.utils import DeferSigint
from ray._private.object_ref_generator import ObjectRefGenerator, DynamicObjectRefGenerator
from ray._private.gc_collect_manager import PythonGCThread
# Expose GCC & Clang macro to report
# whether C++ optimizations were enabled during compilation.
OPTIMIZED = __OPTIMIZE__
GRPC_STATUS_CODE_UNAVAILABLE = CGrpcStatusCode.UNAVAILABLE
GRPC_STATUS_CODE_UNKNOWN = CGrpcStatusCode.UNKNOWN
GRPC_STATUS_CODE_DEADLINE_EXCEEDED = CGrpcStatusCode.DEADLINE_EXCEEDED
GRPC_STATUS_CODE_RESOURCE_EXHAUSTED = CGrpcStatusCode.RESOURCE_EXHAUSTED
GRPC_STATUS_CODE_UNIMPLEMENTED = CGrpcStatusCode.UNIMPLEMENTED
logger = logging.getLogger(__name__)
import warnings
class NumReturnsWarning(UserWarning):
"""Warning when num_returns=0 but the task returns a non-None value."""
pass
warnings.filterwarnings("once", category=NumReturnsWarning)
# The currently running task, if any. These are used to synchronize task
# interruption for ray.cancel.
current_task_id = None
current_task_id_lock = threading.Lock()
# Task ids of the tasks (there can be >1 exit tasks when max_concurrency > 1)
# that called exit_actor(). Used to ensure that for tasks that called exit_actor(),
# their results are discarded (the caller sees the actor death error) even
# if user code swallows the resulting exception — while other tasks that complete
# during the graceful exit still deliver their results.
# Guarded by exit_actor_task_ids_lock since concurrent actors mutate it from
# multiple worker threads.
exit_actor_task_ids = set()
exit_actor_task_ids_lock = threading.Lock()
job_config_initialized = False
job_config_initialization_lock = threading.Lock()
# It is used to indicate std::nullopt for
# AllocateDynamicReturnId.
cdef optional[ObjectIDIndexType] NULL_PUT_INDEX = nullopt
# Used to indicate std::nullopt for tensor_transport.
cdef optional[c_string] NULL_TENSOR_TRANSPORT = nullopt
# This argument is used to obtain the correct task id inside
# an asyncio task. It is because task_id can be obtained
# by the worker_context_ API, which is per thread, not per
# asyncio task. TODO(sang): We should properly fix it.
# Note that the context var is recommended to be defined
# in the top module.
# https://docs.python.org/3/library/contextvars.html#contextvars.ContextVar
# It is thread-safe.
async_task_id = contextvars.ContextVar('async_task_id', default=None)
async_task_name = contextvars.ContextVar('async_task_name', default=None)
async_task_function_name = contextvars.ContextVar('async_task_function_name', default=None)
# Update the type names of the extension type so they are
# ray.{ObjectRef, ObjectRefGenerator} instead of ray._raylet.*
# For ObjectRefGenerator that can be done directly since it is
# a full Python class. For ObjectRef we need to update the
# tp_name since it is a C extension class and not a full class.
cdef PyTypeObject* object_ref_py_type = <PyTypeObject*>ObjectRef
object_ref_py_type.tp_name = "ray.ObjectRef"
ObjectRefGenerator.__module__ = "ray"
# For backward compatibility.
StreamingObjectRefGenerator = ObjectRefGenerator
cdef c_bool is_plasma_object(shared_ptr[CRayObject] obj):
"""Return True if the given object is a plasma object."""
assert obj.get() != NULL
if (obj.get().GetData().get() != NULL
and obj.get().GetData().get().IsPlasmaBuffer()):
return True
return False
class SerializedRayObject(NamedTuple):
data: Optional[Buffer]
metadata: Optional[Buffer]
# If set to None, use the default object store transport. Data will be
# either inlined in `data` or found in the plasma object store.
tensor_transport: Optional[str]
cdef RayObjectsToSerializedRayObjects(
const c_vector[shared_ptr[CRayObject]] objects, object_refs: Optional[List[ObjectRef]] = None):
serialized_ray_objects = []
for i in range(objects.size()):
# core_worker will return a nullptr for objects that couldn't be
# retrieved from the store or if an object was an exception.
if not objects[i].get():
serialized_ray_objects.append(SerializedRayObject(None, None, None))
else:
data = None
metadata = None
if objects[i].get().HasData():
data = Buffer.make(objects[i].get().GetData())
if objects[i].get().HasMetadata():
metadata = Buffer.make(
objects[i].get().GetMetadata()).to_pybytes()
c_tensor_transport = objects[i].get().GetTensorTransport()
tensor_transport = None
if (
not c_tensor_transport.has_value()
and object_refs is not None
):
tensor_transport = object_refs[i].tensor_transport()
elif c_tensor_transport.has_value():
tensor_transport = c_tensor_transport.value().decode("utf-8")
serialized_ray_objects.append(SerializedRayObject(data, metadata, tensor_transport))
return serialized_ray_objects
cdef VectorToObjectRefs(const c_vector[CObjectReference] &object_refs,
skip_adding_local_ref):
result = []
for i in range(object_refs.size()):
tensor_transport = None
if object_refs[i].has_tensor_transport():
tensor_transport = object_refs[i].tensor_transport().decode("utf-8")
result.append(ObjectRef(
object_refs[i].object_id(),
object_refs[i].owner_address().SerializeAsString(),
object_refs[i].call_site(),
skip_adding_local_ref,
tensor_transport))
return result
cdef c_vector[CObjectID] ObjectRefsToVector(object_refs):
"""A helper function that converts a Python list of object refs to a vector.
Args:
object_refs (list): The Python list of object refs.
Returns:
The output vector.
"""
cdef:
c_vector[CObjectID] result
for object_ref in object_refs:
result.push_back((<ObjectRef>object_ref).native())
return result
def compute_task_id(ObjectRef object_ref):
return TaskID(object_ref.native().TaskId().Binary())
def get_port_filename(node_id: str, port_name: str) -> str:
cdef CNodeID c_node_id = CNodeID.FromHex(node_id)
return GetPortFileName(c_node_id, port_name.encode()).decode()
def persist_port(dir: str, node_id: str, port_name: str, port: int) -> None:
cdef CNodeID c_node_id = CNodeID.FromHex(node_id)
cdef CRayStatus status = PersistPort(
dir.encode(), c_node_id, port_name.encode(), port)
if not status.ok():
raise RuntimeError(status.message().decode())
def wait_for_persisted_port(
dir: str,
node_id: str,
port_name: str,
timeout_ms: int = 30000,
poll_interval_ms: int = 100
) -> int:
cdef CNodeID c_node_id = CNodeID.FromHex(node_id)
cdef CWaitForPersistedPortResult result = WaitForPersistedPort(
dir.encode(), c_node_id, port_name.encode(), timeout_ms, poll_interval_ms)
if not result.has_value():
raise RuntimeError(result.message().decode())
return result.value()
cdef increase_recursion_limit():
"""
Ray does some weird things with asio fibers and asyncio to run asyncio actors.
This results in the Python interpreter thinking there's a lot of recursion depth,
so we need to increase the limit when we start getting close.
0x30E0000 is Python 3.14+
On 3.14+, when recursion depth increases, py_recursion_remaining will decrease
(renamed from c_recursion_remaining in 3.12-3.13).
Increasing it by 1000 when it drops below 1000 will keep us from raising the RecursionError.
0x30C0000 is Python 3.12-3.13
On 3.12-3.13, when recursion depth increases, c_recursion_remaining will decrease,
and that's what's actually compared to raise a RecursionError. So increasing
it by 1000 when it drops below 1000 will keep us from raising the RecursionError.
https://github.com/python/cpython/blob/bfb9e2f4a4e690099ec2ec53c08b90f4d64fde36/Python/pystate.c#L1353
0x30B00A4 is Python 3.11
On 3.11, the recursion depth can be calculated with recursion_limit - recursion_remaining.
We can get the current limit with Py_GetRecursionLimit and set it with Py_SetRecursionLimit.
We'll double the limit when there's less than 500 remaining.
On older versions
There's simply a recursion_depth variable and we'll increase the max the same
way we do for 3.11.
"""
cdef:
cdef extern from *:
"""
#if PY_VERSION_HEX >= 0x30E0000
// Python 3.14+ renamed c_recursion_remaining to py_recursion_remaining
bool IncreaseRecursionLimitIfNeeded(PyThreadState *x) {
if (x->py_recursion_remaining < 1000) {
x->py_recursion_remaining += 1000;
return true;
}
return false;
}
#elif PY_VERSION_HEX >= 0x30C0000
// Python 3.12-3.13 use c_recursion_remaining
bool IncreaseRecursionLimitIfNeeded(PyThreadState *x) {
if (x->c_recursion_remaining < 1000) {
x->c_recursion_remaining += 1000;
return true;
}
return false;
}
#elif PY_VERSION_HEX >= 0x30B00A4
bool IncreaseRecursionLimitIfNeeded(PyThreadState *x) {
int current_limit = Py_GetRecursionLimit();
int current_depth = x->recursion_limit - x->recursion_remaining;
if (current_limit - current_depth < 500) {
Py_SetRecursionLimit(current_limit * 2);
return true;
}
return false;
}
#else
bool IncreaseRecursionLimitIfNeeded(PyThreadState *x) {
int current_limit = Py_GetRecursionLimit();
if (current_limit - x->recursion_depth < 500) {
Py_SetRecursionLimit(current_limit * 2);
return true;
}
return false;
}
#endif
"""
c_bool IncreaseRecursionLimitIfNeeded(CPyThreadState *x)
CPyThreadState * s = <CPyThreadState *> PyThreadState_Get()
c_bool increased_recursion_limit = IncreaseRecursionLimitIfNeeded(s)
if increased_recursion_limit:
logger.debug("Increased Python recursion limit")
cdef CObjectLocationPtrToDict(CObjectLocation* c_object_location):
"""A helper function that converts a CObjectLocation to a Python dict.
Returns:
A Dict with following attributes:
- node_ids:
The hex IDs of the nodes that have a copy of this object.
- object_size:
The size of data + metadata in bytes. Can be None if it's -1 in the source.
- did_spill:
Whether or not this object was spilled.
"""
object_size = c_object_location.GetObjectSize()
if object_size <= 0:
object_size = None
did_spill = c_object_location.GetDidSpill()
node_ids = set()
c_node_ids = c_object_location.GetNodeIDs()
for i in range(c_node_ids.size()):
node_id = c_node_ids[i].Hex().decode("ascii")
node_ids.add(node_id)
# add spilled_node_id into node_ids
if not c_object_location.GetSpilledNodeID().IsNil():
node_ids.add(
c_object_location.GetSpilledNodeID().Hex().decode("ascii"))
return {
"node_ids": list(node_ids),
"object_size": object_size,
"did_spill": did_spill,
}
@cython.auto_pickle(False)
cdef class Language:
cdef CLanguage lang
def __cinit__(self, int32_t lang):
self.lang = <CLanguage>lang
@staticmethod
cdef from_native(const CLanguage& lang):
return Language(<int32_t>lang)
def value(self):
return <int32_t>self.lang
def __eq__(self, other):
return (isinstance(other, Language) and
(<int32_t>self.lang) == (<int32_t>(<Language>other).lang))
def __repr__(self):
if <int32_t>self.lang == <int32_t>LANGUAGE_PYTHON:
return "PYTHON"
elif <int32_t>self.lang == <int32_t>LANGUAGE_CPP:
return "CPP"
elif <int32_t>self.lang == <int32_t>LANGUAGE_JAVA:
return "JAVA"
else:
raise Exception("Unexpected error")
def __reduce__(self):
return Language, (<int32_t>self.lang,)
PYTHON = Language.from_native(LANGUAGE_PYTHON)
CPP = Language.from_native(LANGUAGE_CPP)
JAVA = Language.from_native(LANGUAGE_JAVA)
cdef CPlacementStrategy prepare_c_strategy(c_string strategy) except *:
# Called by CoreWorker.create_placement_group(..., c_string strategy, ...).
# The Python placement_group wrapper validates `strategy` to be one of the
# strategies below beforehand.
if strategy == b"PACK":
return PLACEMENT_STRATEGY_PACK
elif strategy == b"SPREAD":
return PLACEMENT_STRATEGY_SPREAD
elif strategy == b"STRICT_PACK":
return PLACEMENT_STRATEGY_STRICT_PACK
else:
return PLACEMENT_STRATEGY_STRICT_SPREAD
def raise_sys_exit_with_custom_error_message(
ray_terminate_msg: str,
exit_code: int = 0) -> None:
"""It is equivalent to sys.exit, but it can contain
a custom message. Custom message is reported to
raylet and is accessible via GCS (from `ray get workers`).
Note that sys.exit == raise SystemExit. I.e., this API
simply raises SystemExit with a custom error message accessible
via `e.ray_terminate_msg`.
Args:
ray_terminate_msg: The error message to propagate to GCS.
exit_code: The exit code. If it is not 0, it is considered
as a system error.
"""
# Raising SystemExit(0) is equivalent to
# sys.exit(0).
# https://docs.python.org/3/library/exceptions.html#SystemExit
e = SystemExit(exit_code)
e.ray_terminate_msg = ray_terminate_msg
raise e
cdef prepare_args_and_increment_put_refs(
Language language, args,
c_vector[unique_ptr[CTaskArg]] *args_vector, function_descriptor,
c_vector[CObjectID] *incremented_put_arg_ids):
try:
prepare_args_internal(language, args, args_vector,
function_descriptor, incremented_put_arg_ids)
except Exception as e:
# An error occurred during arg serialization. We must remove the
# initial local ref for all args that were successfully put into the
# local plasma store. These objects will then get released.
for put_arg_id in dereference(incremented_put_arg_ids):
CCoreWorkerProcess.GetCoreWorker().RemoveLocalReference(
put_arg_id)
raise e
cdef prepare_args_internal(
Language language, args,
c_vector[unique_ptr[CTaskArg]] *args_vector, function_descriptor,
c_vector[CObjectID] *incremented_put_arg_ids):
"""Serializes, reference count, and optionally stores arguments in the Object Store.
Args:
language: used to inspect the serialized metadata of arguments that are not ObjectRefs.
args_vector[out]: used to return remote function references or values.
function_descriptor: used to build a detailed error message if serialization fails.
incremented_put_arg_ids[out]: arguments that were added to the Object Store and therefore
must have their reference counts decremented after the task is submitted.
There are two semantics for passing arguments to remote functions:
1. pass-by-reference
2. pass-by-value
If an argument is an ObjectRef, it is always passed-by-ref. Ray already knows about this
argument therefore it does not need to be serialized and its reference count does not need
to be incremented.
If an argument is not an ObjectRef, it needs to be serialized so it can be transported. If
the argument is small and there are enough bytes available in the transport buffer, the argument
will be passed-by-value. No reference counting is necessary for pass-by-value. If the argument is not
passed-by-value, then it is put into the object store and reference counted.
Raises:
TypeError: If an argument is a CompiledDAGRef or if an argument is not an ObjectRef and cannot be
serialized.
Exception: If the language is not python and the serialized metadata is unrecognized.
"""
cdef:
size_t size
int64_t put_threshold
int64_t rpc_inline_threshold
int64_t total_inlined
shared_ptr[CBuffer] arg_data
c_vector[CObjectID] inlined_ids
c_string put_arg_call_site
c_vector[CObjectReference] inlined_refs
CAddress c_owner_address
CRayStatus op_status
optional[c_string] c_tensor_transport = NULL_TENSOR_TRANSPORT
worker = ray._private.worker.global_worker
put_threshold = RayConfig.instance().max_direct_call_object_size()
total_inlined = 0
rpc_inline_threshold = RayConfig.instance().task_rpc_inlined_bytes_limit()
serialization_context = worker.get_serialization_context()
for arg in args:
from ray.experimental.compiled_dag_ref import CompiledDAGRef
if isinstance(arg, CompiledDAGRef):
raise TypeError("CompiledDAGRef cannot be used as Ray task/actor argument.")
if isinstance(arg, ObjectRef):
c_arg = (<ObjectRef>arg).native()
op_status = CCoreWorkerProcess.GetCoreWorker().GetOwnerAddress(
c_arg, &c_owner_address)
check_status(op_status)
c_tensor_transport = (<ObjectRef>arg).c_tensor_transport()
args_vector.push_back(
unique_ptr[CTaskArg](new CTaskArgByReference(
c_arg,
c_owner_address,
arg.call_site(),
move(c_tensor_transport))))
c_tensor_transport = NULL_TENSOR_TRANSPORT
else:
try:
serialized_arg = serialization_context.serialize(arg)
except TypeError as e:
sio = io.StringIO()
ray.util.inspect_serializability(arg, print_file=sio)
msg = (
"Could not serialize the argument "
f"{repr(arg)} for a task or actor "
f"{function_descriptor.repr}:\n"
f"{sio.getvalue()}")
raise TypeError(msg) from e
metadata = serialized_arg.metadata
if language != Language.PYTHON:
metadata_fields = metadata.split(b",")
if metadata_fields[0] not in [
ray_constants.OBJECT_METADATA_TYPE_CROSS_LANGUAGE,
ray_constants.OBJECT_METADATA_TYPE_RAW,
ray_constants.OBJECT_METADATA_TYPE_ACTOR_HANDLE]:
raise Exception("Can't transfer {} data to {}".format(
metadata_fields[0], language))
size = serialized_arg.total_bytes
if RayConfig.instance().record_ref_creation_sites():
get_py_stack(&put_arg_call_site)
if <int64_t>size <= put_threshold and \
(<int64_t>size + total_inlined <= rpc_inline_threshold):
arg_data = dynamic_pointer_cast[CBuffer, LocalMemoryBuffer](
make_shared[LocalMemoryBuffer](size))
if size > 0:
(<SerializedObject>serialized_arg).write_to(
Buffer.make(arg_data))
for object_ref in serialized_arg.contained_object_refs:
inlined_ids.push_back((<ObjectRef>object_ref).native())
inlined_refs = (CCoreWorkerProcess.GetCoreWorker()
.GetObjectRefs(inlined_ids))
args_vector.push_back(
unique_ptr[CTaskArg](new CTaskArgByValue(
make_shared[CRayObject](
arg_data, string_to_buffer(metadata),
inlined_refs))))
inlined_ids.clear()
total_inlined += <int64_t>size
else:
put_id = CObjectID.FromBinary(
(<CoreWorker>worker.core_worker).put_serialized_object_and_increment_local_ref(
serialized_arg, c_tensor_transport, pin_object=True,
inline_small_object=False))
args_vector.push_back(unique_ptr[CTaskArg](
new CTaskArgByReference(
put_id,
CCoreWorkerProcess.GetCoreWorker().GetRpcAddress(),
put_arg_call_site,
c_tensor_transport
)))
incremented_put_arg_ids.push_back(put_id)
cdef raise_if_dependency_failed(arg):
"""This method is used to improve the readability of backtrace.
With this method, the backtrace will always contain
raise_if_dependency_failed when the task is failed with dependency
failures.
"""
if isinstance(arg, RayError):
raise arg
def serialize_retry_exception_allowlist(retry_exception_allowlist, function_descriptor):
try:
return ray_pickle.dumps(retry_exception_allowlist)
except TypeError as e:
msg = (
"Could not serialize the retry exception allowlist"
f"{retry_exception_allowlist} for task {function_descriptor.repr}. "
"See "
"https://docs.ray.io/en/master/ray-core/objects/serialization.html#troubleshooting " # noqa
"for more information.")
raise TypeError(msg) from e
cdef c_bool determine_if_retryable(
c_bool should_retry_exceptions,
e: BaseException,
const c_string serialized_retry_exception_allowlist,
FunctionDescriptor function_descriptor,
):
"""Determine if the provided exception is retryable, according to the
(possibly null) serialized exception allowlist.
If the serialized exception allowlist is an empty string or is None once
deserialized, the exception is considered retryable and we return True.
This method can raise an exception if:
- Deserialization of exception allowlist fails (TypeError)
- Exception allowlist is not None and not a tuple (AssertionError)
"""
if not should_retry_exceptions:
return False
if len(serialized_retry_exception_allowlist) == 0:
# No exception allowlist specified, default to all retryable.
return True
# Deserialize exception allowlist and check that the exception is in the allowlist.
try:
exception_allowlist = ray_pickle.loads(
serialized_retry_exception_allowlist,
)
except TypeError as inner_e:
# Exception allowlist deserialization failed.
msg = (
"Could not deserialize the retry exception allowlist "
f"for task {function_descriptor.repr}. "
"Check "
"https://docs.ray.io/en/master/ray-core/objects/serialization.html#troubleshooting " # noqa
"for more information.")
raise TypeError(msg) from inner_e
if exception_allowlist is None:
# No exception allowlist specified, default to all retryable.
return True
# Python API should have converted the list of exceptions to a tuple.
assert isinstance(exception_allowlist, tuple)
# For exceptions raised when running UDFs in Ray Data, we need to unwrap the special
# exception type thrown by Ray Data in order to get the underlying exception.
if isinstance(e, ray.exceptions.UserCodeException):
e = e.__cause__
# Check that e is in allowlist.
return isinstance(e, exception_allowlist)
cdef store_task_errors(
worker,
exc,
task_exception,
actor,
actor_id,
function_name,
CTaskType task_type,
proctitle,
const CAddress &caller_address,
c_vector[c_pair[CObjectID, shared_ptr[CRayObject]]] *returns,
c_string* application_error):
cdef:
CoreWorker core_worker = worker.core_worker
# If the debugger is enabled, drop into the remote pdb here.
if ray.util.pdb._is_ray_debugger_post_mortem_enabled():
ray.util.pdb._post_mortem()
backtrace = ray._private.utils.format_error_message(
"".join(traceback.format_exception(type(exc), exc, exc.__traceback__)),
task_exception=task_exception)
# Generate the actor repr from the actor class.
actor_repr = repr(actor) if actor else None
if actor_id is None or actor_id.is_nil():
actor_id = None
else:
actor_id = actor_id.hex()
if isinstance(exc, RayTaskError):
# Avoid recursive nesting of RayTaskError.
failure_object = RayTaskError(function_name, backtrace,
exc.cause, proctitle=proctitle,
actor_repr=actor_repr,
actor_id=actor_id)
else:
failure_object = RayTaskError(function_name, backtrace,
exc, proctitle=proctitle,
actor_repr=actor_repr,
actor_id=actor_id)
# Pass the failure object back to the CoreWorker.
# We also cap the size of the error message to the last
# MAX_APPLICATION_ERROR_LENGTH characters of the error message.
if application_error != NULL:
if ray_constants.MAX_APPLICATION_ERROR_LENGTH == 0:
application_error[0] = b""
else:
application_error[0] = str(failure_object)[-ray_constants.MAX_APPLICATION_ERROR_LENGTH:]
errors = []
for _ in range(returns[0].size()):
errors.append(failure_object)
num_errors_stored = core_worker.store_task_outputs(
worker, errors,
caller_address,
returns,
None, # ref_generator_id
NULL_TENSOR_TRANSPORT)
if (<int>task_type == <int>TASK_TYPE_ACTOR_CREATION_TASK):
raise ActorDiedError.from_task_error(failure_object)
return num_errors_stored
cdef class StreamingGeneratorExecutionContext:
"""The context to run a streaming generator function.
Make sure you always call `initialize` API before
accessing any fields.
Args:
generator: The generator to run.
generator_id: The object ref id of the generator task.
task_type: The type of the task. E.g., actor task, normal task.
caller_address: The address of the caller. By our protocol,
the caller of the streaming generator task is always
the owner, so we can also call it "owner address".
task_id: The task ID of the generator task.
serialized_retry_exception_allowlist: A list of
exceptions that are allowed to retry this generator task.
function_name: The name of the generator function. Used for
writing an error message.
function_descriptor: The function descriptor of
the generator function. Used for writing an error message.
title: The process title of the generator task. Used for
writing an error message.
actor: The instance of the actor created in this worker.
It is used to write an error message.
actor_id: The ID of the actor. It is used to write an error message.
return_size: The number of static returns.
attempt_number: The number of times the current task is retried.
0 means it is the first execution of the task.
should_retry_exceptions: True if the task should be
retried upon exceptions.
streaming_generator_returns(out): A list of a pair of (ObjectID,
is_plasma_object) that are generated by a streaming generator
task.
is_retryable_error(out): It is set to True if the generator
raises an exception, and the error is retryable.
application_error(out): It is set if the generator raises an
application error.
generator_backpressure_num_objects: The backpressure threshold
for streaming generator. The stremaing generator pauses if
total number of unconsumed objects exceed this threshold.
"""
cdef:
# -- Arguments that are not passed--
# Whether or not a generator is async
object is_async
# True if `initialize` API has been called. False otherwise.
object _is_initialized
# -- Arguments that are passed. See the docstring for details --
object generator
CObjectID generator_id
CTaskType task_type
CAddress caller_address
TaskID task_id
c_string serialized_retry_exception_allowlist
object function_name
object function_descriptor
object title
object actor
object actor_id
object name_of_concurrency_group_to_execute
object return_size
uint64_t attempt_number
c_bool should_retry_exceptions
c_vector[c_pair[CObjectID, c_bool]] *streaming_generator_returns
c_bool *is_retryable_error
c_string *application_error
shared_ptr[CTaskGeneratorBackpressureWaiter] waiter
# Per-task accounting for the actor-wide cap. NULL when the actor
# option `_actor_generator_backpressure_num_objects` is disabled
# (or this is a non-actor task).
shared_ptr[CActorTaskBackpressureMetadata] actor_backpressure_metadata
c_bool actor_backpressure_state_owned_by_core_worker
int64_t num_objects_per_yield
# asyncio.Event + its loop used by async streaming generators to wait for
# backpressure to clear without blocking a thread. The C++ core worker
# wakes the event (via a callback, from the RPC thread that processes
# consumption updates) through SetAsyncGeneratorBackpressureUnblockNotify.
# Only set while an async generator with backpressure is executing.
object backpressure_event
object backpressure_loop
cdef teardown_actor_backpressure_state_if_needed(self):
"""Release the actor-wide BP slot held by this task.
Idempotent and safe to invoke multiple times. Skipped when
``actor_backpressure_state_owned_by_core_worker`` is True as that
flag means a normal-completion path handed ownership of the state
to the C++ core worker, which keeps it alive until downstream
consumers drain the stream.
"""
cdef c_bool state_found
if (
self.actor_backpressure_metadata.get() == NULL
or self.actor_backpressure_state_owned_by_core_worker
):
return
# ``state_found`` reports whether ``CoreWorker::generator_backpressure_states_``
# still has an entry for this generator. It is False when another
# cleanup path already erased it before we got here -- e.g.
# ``HandleUpdateGeneratorBackpressureConsumed`` (after the caller
# drained the stream), ``HandleOwnerDied`` (owner-worker failure),
# or the report-RPC failure callback in
# ``CoreWorker::ReportGeneratorItemReturns``. In that case we still
# call ``Teardown`` directly on the locally-held metadata so the
# actor-wide slot is reclaimed; both calls are no-ops when the
# state has already been reaped (``task_alive`` is false).
state_found = (
CCoreWorkerProcess.GetCoreWorker()
.TeardownGeneratorBackpressureTask(self.generator_id)
)
if not state_found:
self.actor_backpressure_metadata.get().Teardown()
# Teardown reclaimed this task's actor-wide budget and signaled the
# waiter's condition variable for sync reservers; async reservers wait on
# an asyncio.Event instead, so wake them too. Otherwise a sibling async
# generator parked in its actor-wide reserve would stay blocked until
# some other relief path (consumption/owner death) happens to fire.
# GIL released so the notification guard is taken without it.
with nogil:
CCoreWorkerProcess.GetCoreWorker(
).NotifyAsyncGeneratorBackpressureUnblock(
self.generator_id, True)
def initialize(self, generator: Union[Generator, AsyncGenerator]):
# We couldn't make this a part of `make` method because
# It looks like we cannot pass generator to cdef
# function (`make`) in Cython.
self.generator = generator
self.is_async = inspect.isasyncgen(generator)
self._is_initialized = True
def is_initialized(self):
return self._is_initialized
@staticmethod
cdef make(
const CObjectID &generator_id,
CTaskType task_type,
const CAddress &caller_address,
TaskID task_id,
const c_string &serialized_retry_exception_allowlist,
function_name: str,
function_descriptor: FunctionDescriptor,
title: str,
actor: object,
actor_id: ActorID,
name_of_concurrency_group_to_execute: str,
return_size: int,
uint64_t attempt_number,
c_bool should_retry_exceptions,
c_vector[c_pair[CObjectID, c_bool]] *streaming_generator_returns,
c_bool *is_retryable_error,
c_string *application_error,
int64_t generator_backpressure_num_objects,
int64_t num_objects_per_yield,
):
cdef StreamingGeneratorExecutionContext self = (
StreamingGeneratorExecutionContext())
self.function_name = function_name
self.function_descriptor = function_descriptor
self.title = title
self.actor = actor
self.actor_id = actor_id
self.name_of_concurrency_group_to_execute = name_of_concurrency_group_to_execute
self.return_size = return_size
self._is_initialized = False
self.generator_id = generator_id
self.task_type = task_type
self.caller_address = caller_address
self.task_id = task_id
self.serialized_retry_exception_allowlist = serialized_retry_exception_allowlist
self.attempt_number = attempt_number
self.streaming_generator_returns = streaming_generator_returns
self.is_retryable_error = is_retryable_error
self.application_error = application_error
self.should_retry_exceptions = should_retry_exceptions
self.actor_backpressure_state_owned_by_core_worker = False
self.num_objects_per_yield = num_objects_per_yield
self.waiter = make_shared[CTaskGeneratorBackpressureWaiter](
generator_backpressure_num_objects,
check_signals
)
cdef shared_ptr[CActorWideGeneratorBackpressureWaiter] actor_waiter = (
CCoreWorkerProcess.GetCoreWorker().GetActorGeneratorWaiter())
# actor_waiter is null if the actor was created without
# `_actor_generator_backpressure_num_objects > 0`. or this is a non-actor task.
if actor_waiter.get() != NULL:
self.actor_backpressure_metadata = (
make_shared[CActorTaskBackpressureMetadata](actor_waiter))
# Pre-register the backpressure entry up-front so HandleOwnerDied can
# find tasks blocked in ReserveActorWideSlot before they have sent
# their first ReportGeneratorItemReturns (which is the other site that
# writes this entry). Without this, a multi-stream actor whose budget
# is held by other tasks can leave the dying-owner's task parked in
# reserve indefinitely, pinning a concurrency slot.
if (
generator_backpressure_num_objects > 0
or self.actor_backpressure_metadata.get() != NULL
):
CCoreWorkerProcess.GetCoreWorker().RegisterGeneratorBackpressureState(
generator_id,
self.waiter,
self.actor_backpressure_metadata,
caller_address,
)
return self
@dataclass(frozen=True)
class StreamingGeneratorStats:
object_creation_dur_s: float
cdef report_streaming_generator_output(
StreamingGeneratorExecutionContext context,
output: object,
generator_index: int64_t,
interrupt_signal_event: Optional[threading.Event],
):
"""Report a given generator output to a caller.
Args:
context: Streaming generator's execution context.
output: The output yielded from a
generator or raised as an exception.
generator_index: The first ObjectRef stream index for this yield.
"""
worker = ray._private.worker.global_worker
cdef:
# Ray Objects created from an output.
c_vector[c_pair[CObjectID, shared_ptr[CRayObject]]] return_objs
size_t i
c_bool output_error_reported = False
start = time.perf_counter()
# Report the intermediate result if there was no error.
try:
create_generator_return_objs(
output,
context.generator_id,
worker,
context.caller_address,
context.task_id,
context.return_size,
generator_index,
context.num_objects_per_yield,
context.is_async,
&return_objs)
except Exception as e:
if (
context.num_objects_per_yield == 1
or return_objs.size() != <size_t>context.num_objects_per_yield
):
raise
# Dynamic IDs for this grouped yield are already allocated. If storing
# failed after some objects were written, report the whole group as
# non-retryable so the caller does not block waiting for later stream
# indexes and the allocated IDs can be cleared.
context.is_retryable_error[0] = False
store_task_errors(
worker, e,
True, # task_exception
context.actor, # actor
context.actor_id, # actor id
context.function_name, context.task_type, context.title,
context.caller_address,
&return_objs,
context.application_error)
output_error_reported = True
# Del output here so that we can GC the memory
# usage asap.
del output
# NOTE: Once interrupting event is set by the caller, we can NOT access
# externally provided data-structures, and have to interrupt the execution
if interrupt_signal_event is not None and interrupt_signal_event.is_set():
return
for i in range(return_objs.size()):
context.streaming_generator_returns[0].push_back(
c_pair[CObjectID, c_bool](
return_objs[i].first,
is_plasma_object(return_objs[i].second)))
serialization_dur_s = time.perf_counter() - start
with nogil:
check_status(CCoreWorkerProcess.GetCoreWorker().ReportGeneratorItemReturns(
return_objs,
context.generator_id,
context.caller_address,
generator_index,
context.attempt_number,
context.waiter,
context.actor_backpressure_metadata))
if output_error_reported:
return None
return StreamingGeneratorStats(
object_creation_dur_s=serialization_dur_s,
)
cdef report_streaming_generator_exception(
StreamingGeneratorExecutionContext context,
e: Exception,
generator_index: int64_t,
interrupt_signal_event: Optional[threading.Event],
):
"""Report a given generator exception to a caller.
Args:
context: Streaming generator's execution context.
output_or_exception: The output yielded from a
generator or raised as an exception.
generator_index: The ObjectRef stream index for this exception.
"""
worker = ray._private.worker.global_worker
cdef:
# Ray Object created from an output.
c_vector[c_pair[CObjectID, shared_ptr[CRayObject]]] return_objs
create_generator_error_object(
e,
worker,
context.task_type,
context.caller_address,
context.task_id,
context.serialized_retry_exception_allowlist,
context.function_name,
context.function_descriptor,
context.title,
context.actor,
context.actor_id,
context.return_size,
generator_index,
context.is_async,
context.should_retry_exceptions,
&return_objs,
context.is_retryable_error,
context.application_error
)
# Del exception here so that we can GC the memory
# usage asap.
del e
# NOTE: Once interrupting event is set by the caller, we can NOT access
# externally provided data-structures, and have to interrupt the execution
if interrupt_signal_event is not None and interrupt_signal_event.is_set():
return
context.streaming_generator_returns[0].push_back(
c_pair[CObjectID, c_bool](
return_objs[0].first,
is_plasma_object(return_objs[0].second)))
with nogil:
check_status(CCoreWorkerProcess.GetCoreWorker().ReportGeneratorItemReturns(
return_objs,
context.generator_id,
context.caller_address,
generator_index,
context.attempt_number,
context.waiter,
context.actor_backpressure_metadata))
def _reserve_actor_generator_slot(
StreamingGeneratorExecutionContext context):
"""Block (with the GIL released) until the actor-wide generator backpressure budget admits the next yield's objects."""
cdef:
CRayStatus status
int64_t num_objects = context.num_objects_per_yield
with nogil:
status = context.actor_backpressure_metadata.get().ReserveSlot(num_objects)
check_status(status)
def _release_actor_generator_slot(
StreamingGeneratorExecutionContext context):
cdef int64_t num_objects = context.num_objects_per_yield
with nogil:
context.actor_backpressure_metadata.get().ReleaseSlot(num_objects)
def _wait_for_object_consumed(
StreamingGeneratorExecutionContext context):
"""Block (with the GIL released) until the per-task backpressure budget
admits more objects. Used by sync streaming generators (each runs on its own
execution thread, so blocking here is fine). No-op when the per-task option
is disabled (threshold -1)."""
cdef CRayStatus status
with nogil:
status = context.waiter.get().WaitUntilObjectConsumed()
check_status(status)
cdef void _backpressure_unblock_callback(void* ctx) noexcept nogil:
"""C callback invoked by the core worker (from any thread) when an async
streaming generator may have become unblocked. Acquires the GIL and wakes
the generator's asyncio.Event. Registered via
SetAsyncGeneratorBackpressureUnblockNotify; ``ctx`` is the borrowed
StreamingGeneratorExecutionContext, kept alive by the running coroutine.
The Python work lives in a separate GIL-holding helper because this is a
nogil C callback (the core worker calls it without the GIL) and nogil
functions cannot hold Python-object locals."""
with gil:
_notify_backpressure_event(<object>ctx)
cdef _notify_backpressure_event(StreamingGeneratorExecutionContext context):
loop = context.backpressure_loop
event = context.backpressure_event
if loop is None or event is None:
return
try:
loop.call_soon_threadsafe(event.set)
except RuntimeError:
# The event loop is closed/closing; a still-awaiting coroutine is torn
# down through normal cancellation, so there is nothing to wake.
pass
async def _async_wait_for_object_consumed(
StreamingGeneratorExecutionContext context):
"""Await until the per-task backpressure budget admits more objects.
Waits on the generator's asyncio.Event, which the core worker sets from every
path that can relieve backpressure (consumption, owner death, report
failure). No-op when the per-task option is disabled."""
event = context.backpressure_event
while context.waiter.get().IsBackpressured():
# Clear before re-checking so a wake-up delivered between the check and
# the await is not lost.
event.clear()
if not context.waiter.get().IsBackpressured():
break
await event.wait()
async def _async_reserve_actor_generator_slot(
StreamingGeneratorExecutionContext context):
"""Await until the actor-wide budget admits this yield's objects, then
reserve them. Reserves exactly once: ``TryReserveSlot`` admits the group on
success, so it must be called at most once per successful pass.
Waits on the generator's asyncio.Event, which the core worker sets whenever
actor-wide budget may have freed (consumption, a sibling task releasing its
slot, owner death)."""
cdef int64_t num_objects = context.num_objects_per_yield
event = context.backpressure_event
while True:
# Clear before attempting so a wake-up delivered while we attempt (and
# fail) is not lost.
event.clear()
if context.actor_backpressure_metadata.get().TryReserveSlot(num_objects):
break
await event.wait()
cdef execute_streaming_generator_sync(StreamingGeneratorExecutionContext context):
"""Execute a given generator and streaming-report the
result to the given caller_address.
The output from the generator will be stored to the in-memory
or plasma object store. The generated return objects will be
reported to the owner of the task as soon as they are generated.
It means when this method is used, the result of each generator
will be reported and available from the given "caller address"
before the task is finished.
Args:
context: The context to execute streaming generator.
"""
cdef:
int64_t gen_index = 0
CRayStatus return_status
c_bool completed_normally = False
# True if per-task (`_generator_backpressure_num_objects`) backpressure is
# enabled; gates the per-task backpressure wait below. Actor-wide
# backpressure is handled separately by the reserve/release calls.
c_bool per_task_backpressure
assert context.is_initialized()
# Generator task should only have 1 return object ref,
# which contains None or exceptions (if system error occurs).
assert context.return_size == 1
gen = context.generator
per_task_backpressure = context.waiter.get().NeedsObjectConsumedUpdates()
try:
stats = None
while True:
try:
# Actor-wide backpressure pre-check. Block (releasing the
# GIL) until the actor's shared budget admits this yield's
# objects (`_num_objects_per_yield`). No-op when the actor
# option is disabled.
if context.actor_backpressure_metadata.get() != NULL:
_reserve_actor_generator_slot(context)
# Bail before running any more user code if the task has been
# canceled (e.g. the owner died and HandleOwnerDied marked it
# canceled). Placed right before gen.send so it catches wakeups
# from both report_streaming_generator_output (the previous
# iteration's WaitUntilObjectConsumed) and the reserve call
# above; without this we would run the gen body once more
# between yields, which can be arbitrarily expensive.
if CCoreWorkerProcess.GetCoreWorker().IsTaskCanceled(
context.task_id.native()):
break
# Send object serialization duration to the generator and retrieve
# next output
output = gen.send(stats)
# Track serialization duration of the next output
stats = report_streaming_generator_output(
context, output, gen_index, None)
# Per-task backpressure: block until the caller has consumed
# enough ObjectRefs. Skipped when the per-task option is disabled.
# Each sync generator runs on its own execution thread, so
# blocking here does not stall other tasks.
if per_task_backpressure:
_wait_for_object_consumed(context)
if stats is None:
break
gen_index += context.num_objects_per_yield
except StopIteration:
if context.actor_backpressure_metadata.get() != NULL:
_release_actor_generator_slot(context)
# Releasing frees shared actor-wide budget; wake any async
# generator parked in its reserve so it can re-check. (Sync
# reservers are woken by the waiter's condition variable.)
# GIL released so the notification guard is taken without it.
with nogil:
CCoreWorkerProcess.GetCoreWorker(
).NotifyAsyncGeneratorBackpressureUnblock(
context.generator_id, True)
completed_normally = True
break
except Exception as e:
report_streaming_generator_exception(context, e, gen_index, None)
# The caller gets object values through the reports. If we finish the task
# before sending the report is complete, then we may fail before the report
# is sent to the caller. Then, the caller would never be able to ray.get
# the yield'ed ObjectRef. Therefore, we must wait for all in-flight object
# reports to complete before finishing the task.
with nogil:
return_status = context.waiter.get().WaitAllObjectsReported()
check_status(return_status)
if completed_normally or context.actor_backpressure_metadata.get() == NULL:
CCoreWorkerProcess.GetCoreWorker().MarkGeneratorBackpressureTaskFinished(
context.generator_id)
if completed_normally and context.actor_backpressure_metadata.get() != NULL:
# Streaming execution has completed. The C++ CoreWorker keeps actor-wide state alive until downstream
# consumers release the remaining generator items.
context.actor_backpressure_state_owned_by_core_worker = True
context.teardown_actor_backpressure_state_if_needed()
async def execute_streaming_generator_async(
context: StreamingGeneratorExecutionContext):
"""Execute a given generator and report the
result to the given caller_address in a streaming (ie as
soon as become available) fashion.
This method is same as `execute_streaming_generator_sync`,
but it should be used inside an async event loop.
NOTE: since this function runs inside an event loop thread,
some of core worker APIs will be executed inside
the event loop thread as well.
E.g., core_worker.SealOwned can be called.
At this time, if we access worker_context_ API from core worker,
it can cause problems because worker_context_ is configured
per thread (it is a bug & tech debt).
Args:
context: The context to execute streaming generator.
"""
cdef:
int64_t cur_generator_index = 0
CRayStatus return_status
c_bool completed_normally = False
# per_task_backpressure (`_generator_backpressure_num_objects`) gates the
# per-task wait. has_backpressure (per-task OR actor-wide) gates the
# asyncio.Event bridge, which both the per-task wait and the actor-wide
# reserve await.
c_bool per_task_backpressure
c_bool has_backpressure
assert context.is_initialized()
# Generator task should only have 1 return object ref,
# which contains None or exceptions (if system error occurs).
assert context.return_size == 1
gen = context.generator
loop = asyncio.get_running_loop()
worker = ray._private.worker.global_worker
executor = worker.core_worker.get_event_loop_executor()
interrupt_signal_event = threading.Event()
per_task_backpressure = context.waiter.get().NeedsObjectConsumedUpdates()
has_backpressure = (
per_task_backpressure
or context.actor_backpressure_metadata.get() != NULL
)
try:
# Async streaming generators enforce backpressure by awaiting an
# asyncio.Event instead of blocking: the core worker wakes the event
# (via `_backpressure_unblock_callback`) when the caller consumes more
# objects. This keeps the event loop responsive and never holds the
# report executor thread while parked.
#
# Registered INSIDE the try so the finally below always clears it, even
# if setup raises -- a stale registry entry would hold a dangling context.
if has_backpressure:
context.backpressure_loop = loop
context.backpressure_event = asyncio.Event()
# Registered with the GIL released so the registry lock is taken
# without the GIL (the callback acquires the GIL only after); see
# CoreWorker::SetAsyncGeneratorBackpressureUnblockNotify.
with nogil:
CCoreWorkerProcess.GetCoreWorker().SetAsyncGeneratorBackpressureUnblockNotify(
context.generator_id,
_backpressure_unblock_callback,
<void*>context,
)
stats = None
while True:
try:
# Actor-wide backpressure pre-check. Awaits the event (instead of
# blocking the loop) until the shared budget admits this yield's
# objects. Returns immediately when the actor option is disabled.
if context.actor_backpressure_metadata.get() != NULL:
await _async_reserve_actor_generator_slot(context)
# Bail before running any more user code if the task has been
# canceled (e.g. the owner died and HandleOwnerDied marked it
# canceled and tore down the actor metadata, so the reserve above
# returns for the now-dead task). Mirrors the sync path: without
# this the actor would run the gen body once more between yields,
# causing side effects and delaying the actor slot release.
if CCoreWorkerProcess.GetCoreWorker().IsTaskCanceled(
context.task_id.native()):
break
output = await gen.asend(stats)
# NOTE: Report of streaming generator output is done in a
# standalone thread-pool to avoid blocking the event loop,
# since serializing and actual RPC I/O is done with "nogil". We
# still wait for the report to finish to ensure that the task
# does not modify the output before we serialize it.
#
# Note that the RPC is sent asynchronously, and we do not wait
# for the reply here; the per-task backpressure wait is awaited
# separately below.
stats = await loop.run_in_executor(
executor,
report_streaming_generator_output,
context,
output,
cur_generator_index,
interrupt_signal_event,
)
# Per-task backpressure: await until the caller has consumed
# enough ObjectRefs. Skipped when the per-task option is disabled.
if per_task_backpressure:
await _async_wait_for_object_consumed(context)
if stats is None:
break
cur_generator_index += context.num_objects_per_yield
except StopAsyncIteration:
if context.actor_backpressure_metadata.get() != NULL:
# ReleaseSlot is non-blocking; call it directly. Releasing
# frees shared actor-wide budget, so wake any async generator
# parked in its reserve to re-check. GIL released so the
# notification guard is taken without it.
_release_actor_generator_slot(context)
with nogil:
CCoreWorkerProcess.GetCoreWorker(
).NotifyAsyncGeneratorBackpressureUnblock(
context.generator_id, True)
completed_normally = True
break
except Exception as e:
# Report the exception to the owner of the task.
report_streaming_generator_exception(context, e, cur_generator_index, None)
except BaseException as be:
# NOTE: PLEASE READ CAREFULLY BEFORE CHANGING
#
# Upon encountering any failures in reporting generator's output we have to
# make sure that any already scheduled (onto thread-pool executor), but not
# finished tasks are canceled before re-throwing the exception to avoid
# use-after-free failures where tasks could potentially access data-structures
# that are already cleaned by the caller.
#
# For that we set an event to interrupt already scheduled tasks (that have
# not finished executing), therefore interrupting their execution and
# making sure that externally provided data-structures are not
# accessed after this point
#
# For more details, please check out
# https://github.com/ray-project/ray/issues/43771
interrupt_signal_event.set()
raise
finally:
# Stop the core worker from waking a context that is going away. Cleared
# with the GIL released (consistent lock order with the registration).
if has_backpressure:
with nogil:
CCoreWorkerProcess.GetCoreWorker().ClearAsyncGeneratorBackpressureUnblockNotify(
context.generator_id,
)
context.backpressure_event = None
context.backpressure_loop = None
# The caller gets object values through the reports. If we finish the task
# before sending the report is complete, then we may fail before the report
# is sent to the caller. Then, the caller would never be able to ray.get
# the yield'ed ObjectRef. Therefore, we must wait for all in-flight object
# reports to complete before finishing the task.
with nogil:
return_status = context.waiter.get().WaitAllObjectsReported()
check_status(return_status)
if completed_normally or context.actor_backpressure_metadata.get() == NULL:
CCoreWorkerProcess.GetCoreWorker().MarkGeneratorBackpressureTaskFinished(
context.generator_id)
if completed_normally and context.actor_backpressure_metadata.get() != NULL:
# Streaming execution has completed. The C++ CoreWorker keeps actor-wide state alive until downstream
# consumers release the remaining generator items.
context.actor_backpressure_state_owned_by_core_worker = True
context.teardown_actor_backpressure_state_if_needed()
cdef create_generator_return_objs(
output,
const CObjectID &generator_id,
worker: "Worker",
const CAddress &caller_address,
TaskID task_id,
return_size,
generator_index,
int64_t num_objects_per_yield,
is_async,
c_vector[c_pair[CObjectID, shared_ptr[CRayObject]]] *return_objects):
"""Create generator return objects based on a given output.
Args:
output: The output from a next(generator).
generator_id: The object ref id of the generator task.
worker: The Python worker class inside worker.py
caller_address: The address of the caller. By our protocol,
the caller of the streaming generator task is always
the owner, so we can also call it "owner address".
task_id: The task ID of the generator task.
return_size: The number of static returns.
generator_index: The first ObjectRef stream index for this yield.
num_objects_per_yield: The number of ObjectRefs to create for each yield.
is_async: Whether or not the given object is created within
an async actor.
return_objects(out): Ray Objects that contain the given output.
"""
cdef:
CoreWorker core_worker = worker.core_worker
int64_t stream_index
int64_t i
CObjectID return_id
if num_objects_per_yield == 1:
outputs = (output,)
else:
if not isinstance(output, (tuple, list)):
raise ValueError(
"Streaming generator tasks with _num_objects_per_yield="
f"{num_objects_per_yield} must yield a tuple or list "
f"of length {num_objects_per_yield}."
)
if len(output) != num_objects_per_yield:
raise ValueError(
"Streaming generator task yielded "
f"{len(output)} objects, but _num_objects_per_yield="
f"{num_objects_per_yield}."
)
outputs = output
return_objects.reserve(num_objects_per_yield)
for i in range(num_objects_per_yield):
stream_index = generator_index + i
return_id = core_worker.allocate_dynamic_return_id_for_generator(
caller_address,
task_id.native(),
return_size,
stream_index,
is_async,
)
return_objects.push_back(
c_pair[CObjectID, shared_ptr[CRayObject]](
return_id, shared_ptr[CRayObject]()))
core_worker.store_task_outputs(
worker, outputs,
caller_address,
return_objects,
generator_id.Binary())
cdef create_generator_error_object(
e: Exception,
worker: "Worker",
CTaskType task_type,
const CAddress &caller_address,
TaskID task_id,
const c_string &serialized_retry_exception_allowlist,
function_name,
function_descriptor,
title,
actor,
actor_id,
return_size,
generator_index,
is_async,
c_bool should_retry_exceptions,
c_vector[c_pair[CObjectID, shared_ptr[CRayObject]]] *error_objects,
c_bool *is_retryable_error,
c_string *application_error):
"""Create a generator error object.
This API sets is_retryable_error and application_error,
It also creates and returns a new RayObject that
contains the exception `e`.
Args:
e: The exception raised from a generator.
worker: The Python worker class inside worker.py
task_type: The type of the task. E.g., actor task, normal task.
caller_address: The address of the caller. By our protocol,
the caller of the streaming generator task is always
the owner, so we can also call it "owner address".
task_id: The task ID of the generator task.
serialized_retry_exception_allowlist: A list of
exceptions that are allowed to retry this generator task.
function_name: The name of the generator function. Used for
writing an error message.
function_descriptor: The function descriptor of
the generator function. Used for writing an error message.
title: The process title of the generator task. Used for
writing an error message.
actor: The instance of the actor created in this worker.
It is used to write an error message.
actor_id: The ID of the actor. It is used to write an error message.
return_size: The number of static returns.
generator_index: The ObjectRef stream index for this exception.
is_async: Whether or not the given object is created within
an async actor.
error_objects(out): Ray Objects that contain the given error exception.
is_retryable_error(out): It is set to True if the generator
raises an exception, and the error is retryable.
application_error(out): It is set if the generator raises an
application error.
"""
cdef:
CoreWorker core_worker = worker.core_worker
is_retryable_error[0] = determine_if_retryable(
should_retry_exceptions,
e,
serialized_retry_exception_allowlist,
function_descriptor,
)
if is_retryable_error[0]:
logger.debug(
"Task failed with retryable exception:"
" {}.".format(task_id), exc_info=True)
# Raise an exception directly and halt the execution
# because there's no need to set the exception
# for the return value when the task is retryable.
raise e
logger.debug(
"Task failed with unretryable exception:"
" {}.".format(task_id), exc_info=True)
error_id = core_worker.allocate_dynamic_return_id_for_generator(
caller_address,
task_id.native(),
return_size,
generator_index,
is_async,
)
error_objects.push_back(
c_pair[CObjectID, shared_ptr[CRayObject]](
error_id, shared_ptr[CRayObject]()))
store_task_errors(
worker, e,
True, # task_exception
actor, # actor
actor_id, # actor id
function_name, task_type, title,
caller_address,
error_objects,
application_error)
cdef execute_dynamic_generator_and_store_task_outputs(
generator,
const CObjectID &generator_id,
CTaskType task_type,
const c_string &serialized_retry_exception_allowlist,
c_vector[c_pair[CObjectID, shared_ptr[CRayObject]]] *dynamic_returns,
c_bool *is_retryable_error,
c_string *application_error,
c_bool is_reattempt,
function_name,
function_descriptor,
title,
const CAddress &caller_address,
c_bool should_retry_exceptions):
worker = ray._private.worker.global_worker
cdef:
CoreWorker core_worker = worker.core_worker
try:
core_worker.store_task_outputs(
worker, generator,
caller_address,
dynamic_returns,
generator_id.Binary())
except Exception as error:
is_retryable_error[0] = determine_if_retryable(
should_retry_exceptions,
error,
serialized_retry_exception_allowlist,
function_descriptor,
)
if is_retryable_error[0]:
logger.info("Task failed with retryable exception:"
" {}.".format(
core_worker.get_current_task_id()),
exc_info=True)
raise error
else:
logger.debug("Task failed with unretryable exception:"
" {}.".format(
core_worker.get_current_task_id()),
exc_info=True)
if not is_reattempt:
# If this is the first execution, we should
# generate one additional ObjectRef. This last
# ObjectRef will contain the error.
error_id = (CCoreWorkerProcess.GetCoreWorker()
.AllocateDynamicReturnId(
caller_address, CTaskID.Nil(), NULL_PUT_INDEX))
dynamic_returns[0].push_back(
c_pair[CObjectID, shared_ptr[CRayObject]](
error_id, shared_ptr[CRayObject]()))
# If a generator task fails mid-execution, we fail the
# dynamically generated nested ObjectRefs instead of
# the top-level DynamicObjectRefGenerator.
num_errors_stored = store_task_errors(
worker, error,
False, # task_exception
None, # actor
None, # actor id
function_name, task_type, title, caller_address,
dynamic_returns,
application_error)
if num_errors_stored == 0:
assert is_reattempt
# TODO(swang): The generator task failed and we
# also failed to store the error in any of its
# return values. This should only occur if the
# generator task was re-executed and returned more
# values than the initial execution.
logger.error(
"Unhandled error: Re-executed generator task "
"returned more than the "
f"{dynamic_returns[0].size()} values returned "
"by the first execution.\n"
"See https://github.com/ray-project/ray/issues/28688.")
cdef void execute_task(
const CAddress &caller_address,
CTaskType task_type,
const c_string name,
const CRayFunction &ray_function,
const unordered_map[c_string, double] &c_resources,
const c_vector[shared_ptr[CRayObject]] &c_args,
const c_vector[CObjectReference] &c_arg_refs,
const c_string debugger_breakpoint,
const c_string serialized_retry_exception_allowlist,
c_vector[c_pair[CObjectID, shared_ptr[CRayObject]]] *returns,
c_vector[c_pair[CObjectID, shared_ptr[CRayObject]]] *dynamic_returns,
c_vector[c_pair[CObjectID, c_bool]] *streaming_generator_returns,
c_bool *is_retryable_error,
c_string *actor_repr_name,
c_string *application_error,
# This parameter is only used for actor creation task to define
# the concurrency groups of this actor.
const c_vector[CConcurrencyGroup] &c_defined_concurrency_groups,
const c_string c_name_of_concurrency_group_to_execute,
c_bool is_reattempt,
execution_info,
title,
task_name,
c_bool is_streaming_generator,
c_bool should_retry_exceptions,
int64_t generator_backpressure_num_objects,
int64_t num_objects_per_yield,
optional[c_string] c_tensor_transport) except *:
worker = ray._private.worker.global_worker
manager = worker.function_actor_manager
actor = None
actor_id = None
cdef:
CoreWorker core_worker = worker.core_worker
JobID job_id = core_worker.get_current_job_id()
TaskID task_id = core_worker.get_current_task_id()
uint64_t attempt_number = core_worker.get_current_task_attempt_number()
# Helper method used to exit current asyncio actor.
# This is called when a KeyboardInterrupt is received by the main thread.
# Upon receiving a KeyboardInterrupt signal, Ray will exit the current
# worker. If the worker is processing normal tasks, Ray treat it as task
# cancellation from ray.cancel(object_ref). If the worker is an asyncio
# actor, Ray will exit the actor.
def exit_current_actor_if_asyncio():
if core_worker.current_actor_is_asyncio():
raise_sys_exit_with_custom_error_message("exit_actor() is called.")
function_descriptor = CFunctionDescriptorToPython(
ray_function.GetFunctionDescriptor())
function_name = execution_info.function_name
extra_data = (b'{"name": "' + function_name.encode("ascii") +
b'", "task_id": "' + task_id.hex().encode("ascii") + b'"}')
name_of_concurrency_group_to_execute = \
c_name_of_concurrency_group_to_execute.decode("ascii")
if <int>task_type == <int>TASK_TYPE_NORMAL_TASK:
next_title = "ray::IDLE"
function_executor = execution_info.function
# Record the task name via :task_name: magic token in the log file.
# This is used for the prefix in driver logs `(task_name pid=123) ...`
task_name_magic_token = "{}{}\n".format(
ray_constants.LOG_PREFIX_TASK_NAME, task_name.replace("()", ""))
# Print on both .out and .err
print(task_name_magic_token, end="")
print(task_name_magic_token, file=sys.stderr, end="")
else:
actor_id = core_worker.get_actor_id()
actor = worker.actors[actor_id]
class_name = actor.__class__.__name__
next_title = f"ray::{class_name}"
def function_executor(*arguments, **kwarguments):
func = execution_info.function
if core_worker.current_actor_is_asyncio():
if not has_async_methods(actor.__class__):
error_message = (
"Failed to create actor. You set the async flag, "
"but the actor does not "
"have any coroutine functions.")
raise ActorDiedError(
ActorDiedErrorContext(
error_message=error_message,
actor_id=core_worker.get_actor_id().binary(),
class_name=class_name
)
)
if is_async_func(func.method):
async_function = func
else:
# Just execute the method if it's ray internal method.
if func.name.startswith("__ray"):
return func(actor, *arguments, **kwarguments)
async_function = sync_to_async(func)
if inspect.isasyncgenfunction(func.method):
# The coroutine will be handled separately by
# execute_dynamic_generator_and_store_task_outputs
return async_function(actor, *arguments, **kwarguments)
else:
return core_worker.run_async_func_or_coro_in_event_loop(
async_function, function_descriptor,
name_of_concurrency_group_to_execute, task_id=task_id,
task_name=task_name, func_args=(actor, *arguments),
func_kwargs=kwarguments)
return func(actor, *arguments, **kwarguments)
with core_worker.profile_event(b"task::" + name, extra_data=extra_data), \
ray._private.worker._changeproctitle(title, next_title):
task_exception = False
try:
with core_worker.profile_event(b"task:deserialize_arguments"):
if c_args.empty():
args, kwargs = [], {}
else:
object_refs = VectorToObjectRefs(
c_arg_refs,
skip_adding_local_ref=False)
metadata_pairs = RayObjectsToSerializedRayObjects(c_args, object_refs)
if core_worker.current_actor_is_asyncio():
# We deserialize objects in event loop thread to
# prevent segfaults. See #7799
async def deserialize_args():
return (ray._private.worker.global_worker
.deserialize_objects(
metadata_pairs, object_refs))
args = core_worker.run_async_func_or_coro_in_event_loop(
deserialize_args, function_descriptor,
name_of_concurrency_group_to_execute)
else:
# Defer task cancellation (SIGINT) until after the task argument
# deserialization context has been left.
# NOTE (Clark): We defer SIGINT until after task argument
# deserialization completes to keep from interrupting
# non-reentrant imports that may be re-entered during error
# serialization or storage.
# See https://github.com/ray-project/ray/issues/30453.
# NOTE (Clark): Signal handlers can only be registered on the
# main thread.
with DeferSigint.create_if_main_thread():
args = (ray._private.worker.global_worker
.deserialize_objects(
metadata_pairs, object_refs))
for arg in args:
raise_if_dependency_failed(arg)
args, kwargs = ray._common.signature.recover_args(args)
if (<int>task_type == <int>TASK_TYPE_ACTOR_CREATION_TASK):
actor_id = core_worker.get_actor_id()
actor = worker.actors[actor_id]
worker.record_task_log_start(task_id, attempt_number)
# Execute the task.
with core_worker.profile_event(b"task:execute"):
task_exception = True
task_exception_instance = None
try:
if debugger_breakpoint != b"":
ray.util.pdb.set_trace(
breakpoint_uuid=debugger_breakpoint)
outputs = function_executor(*args, **kwargs)
if is_streaming_generator:
# Streaming generator always has a single return value
# which is the generator task return.
assert returns[0].size() == 1
is_async_gen = inspect.isasyncgen(outputs)
is_sync_gen = inspect.isgenerator(outputs)
if (not is_sync_gen
and not is_async_gen):
raise ValueError(
"Functions with "
"@ray.remote(num_returns=\"streaming\" "
"must return a generator")
context = StreamingGeneratorExecutionContext.make(
returns[0][0].first, # generator object ID.
task_type,
caller_address,
task_id,
serialized_retry_exception_allowlist,
function_name,
function_descriptor,
title,
actor,
actor_id,
name_of_concurrency_group_to_execute,
returns[0].size(),
attempt_number,
should_retry_exceptions,
streaming_generator_returns,
is_retryable_error,
application_error,
generator_backpressure_num_objects,
num_objects_per_yield)
# We cannot pass generator to cdef in Cython for some reasons.
# It is a workaround.
context.initialize(outputs)
if is_async_gen:
# Note that the report RPCs are called inside an
# event loop thread.
core_worker.run_async_func_or_coro_in_event_loop(
execute_streaming_generator_async(context),
function_descriptor,
name_of_concurrency_group_to_execute,
task_id=task_id,
task_name=task_name)
else:
execute_streaming_generator_sync(context)
# Streaming generator output is not used, so set it to None.
outputs = None
next_breakpoint = (
ray._private.worker.global_worker.debugger_breakpoint)
if next_breakpoint != b"":
# If this happens, the user typed "remote" and
# there were no more remote calls left in this
# task. In that case we just exit the debugger.
ray.experimental.internal_kv._internal_kv_put(
"RAY_PDB_{}".format(next_breakpoint),
"{\"exit_debugger\": true}",
namespace=ray_constants.KV_NAMESPACE_PDB
)
ray.experimental.internal_kv._internal_kv_del(
"RAY_PDB_CONTINUE_{}".format(next_breakpoint),
namespace=ray_constants.KV_NAMESPACE_PDB
)
(ray._private.worker.global_worker
.debugger_breakpoint) = b""
task_exception = False
except AsyncioActorExit as e:
exit_current_actor_if_asyncio()
except (KeyboardInterrupt, SystemExit):
# Special casing these two because Ray can raise them
raise
except BaseException as e:
is_retryable_error[0] = determine_if_retryable(
should_retry_exceptions,
e,
serialized_retry_exception_allowlist,
function_descriptor,
)
if is_retryable_error[0]:
logger.debug("Task failed with retryable exception:"
" {}.".format(
core_worker.get_current_task_id()),
exc_info=True)
else:
logger.debug("Task failed with unretryable exception:"
" {}.".format(
core_worker.get_current_task_id()),
exc_info=True)
task_exception_instance = e
finally:
# Record the end of the task log.
worker.record_task_log_end(task_id, attempt_number)
if task_exception_instance is not None:
raise task_exception_instance
with exit_actor_task_ids_lock:
this_task_called_exit_actor = task_id in exit_actor_task_ids
exit_actor_task_ids.discard(task_id)
if this_task_called_exit_actor:
# exit_actor() records the task id and sets the
# should-exit flag (which is never cleared) before
# raising, so the flag must be set here.
assert core_worker.get_current_actor_should_exit(), (
"exit_actor() recorded this task id but the "
"actor-should-exit flag is not set."
)
# This task called exit_actor(). Exit before storing
# its outputs even if user code swallowed the
# resulting exception, so the caller sees the actor
# death instead of a return value.
raise_sys_exit_with_custom_error_message(
"exit_actor() is called.")
if (returns[0].size() == 1
and not inspect.isgenerator(outputs)
and not inspect.isasyncgen(outputs)):
# If there is only one return specified, we should return
# all return values as a single object.
outputs = (outputs,)
if (<int>task_type == <int>TASK_TYPE_ACTOR_CREATION_TASK):
# Record actor repr via :actor_name: magic token in the log.
#
# (Phase 2): after `__init__` finishes, we override the
# log prefix with the full repr of the actor. The log monitor
# will pick up the updated token.
actor_class = manager.load_actor_class(job_id, function_descriptor)
if (hasattr(actor_class, "__ray_actor_class__") and
(actor_class.__ray_actor_class__.__repr__
!= object.__repr__)):
actor_repr_str = repr(actor)
actor_magic_token = "{}{}\n".format(
ray_constants.LOG_PREFIX_ACTOR_NAME, actor_repr_str)
# Flush on both stdout and stderr.
print(actor_magic_token, end="")
print(actor_magic_token, file=sys.stderr, end="")
actor_repr_name[0] = actor_repr_str
if (returns[0].size() > 0
and not inspect.isgenerator(outputs)
and not inspect.isasyncgen(outputs)
and len(outputs) != int(returns[0].size())):
raise ValueError(
"Task returned {} objects, but num_returns={}.".format(
len(outputs), returns[0].size()))
# Store the outputs in the object store.
with core_worker.profile_event(b"task:store_outputs"):
# TODO(sang): Remove it once we use streaming generator
# by default.
if dynamic_returns != NULL and not is_streaming_generator:
if not inspect.isgenerator(outputs):
raise ValueError(
"Functions with "
"@ray.remote(num_returns=\"dynamic\" must return a "
"generator")
task_exception = True
execute_dynamic_generator_and_store_task_outputs(
outputs,
returns[0][0].first,
task_type,
serialized_retry_exception_allowlist,
dynamic_returns,
is_retryable_error,
application_error,
is_reattempt,
function_name,
function_descriptor,
title,
caller_address,
should_retry_exceptions)
task_exception = False
dynamic_refs = collections.deque()
for idx in range(dynamic_returns.size()):
dynamic_refs.append(ObjectRef(
dynamic_returns[0][idx].first.Binary(),
caller_address.SerializeAsString(),
))
# Swap out the generator for an ObjectRef generator.
outputs = (DynamicObjectRefGenerator(dynamic_refs), )
# TODO(swang): For generator tasks, iterating over outputs will
# actually run the task. We should run the usual handlers for
# task cancellation, retrying on application exception, etc. for
# all generator tasks, both static and dynamic.
core_worker.store_task_outputs(
worker, outputs,
caller_address,
returns,
None, # ref_generator_id
c_tensor_transport
)
except (KeyboardInterrupt, SystemExit):
# Special casing these two because Ray can raise them
raise
except BaseException as e:
num_errors_stored = store_task_errors(
worker, e, task_exception, actor, actor_id, function_name,
task_type, title, caller_address, returns, application_error)
if returns[0].size() > 0 and num_errors_stored == 0:
logger.exception(
"Unhandled error: Task threw exception, but all "
f"{returns[0].size()} return values already created. "
"This should only occur when using generator tasks.\n"
"See https://github.com/ray-project/ray/issues/28689.")
finally:
# exit_actor() sets a worker-wide flag that every task must check, so
# that the worker exits even when exit_actor() is called from a
# concurrently running task (threaded/async actors) or a background
# thread rather than this task itself. The check must run only after
# the task's outputs (or errors) have been stored above;
# Skip the check if an exception is propagating: it is either an exit
# or cancellation path (SystemExit/KeyboardInterrupt) or an internal
# error that must surface as such, and it must not be masked by
# raising from a finally block.
if (sys.exc_info()[0] is None
and core_worker.get_current_actor_should_exit()):
raise_sys_exit_with_custom_error_message("exit_actor() is called.")
cdef execute_task_with_cancellation_handler(
const CAddress &caller_address,
CTaskType task_type,
const c_string name,
const CRayFunction &ray_function,
const unordered_map[c_string, double] &c_resources,
const c_vector[shared_ptr[CRayObject]] &c_args,
const c_vector[CObjectReference] &c_arg_refs,
const c_string debugger_breakpoint,
const c_string serialized_retry_exception_allowlist,
c_vector[c_pair[CObjectID, shared_ptr[CRayObject]]] *returns,
c_vector[c_pair[CObjectID, shared_ptr[CRayObject]]] *dynamic_returns,
c_vector[c_pair[CObjectID, c_bool]] *streaming_generator_returns,
c_bool *is_retryable_error,
c_string *actor_repr_name,
c_string *application_error,
# This parameter is only used for actor creation task to define
# the concurrency groups of this actor.
const c_vector[CConcurrencyGroup] &c_defined_concurrency_groups,
const c_string c_name_of_concurrency_group_to_execute,
c_bool is_reattempt,
c_bool is_streaming_generator,
c_bool should_retry_exceptions,
int64_t generator_backpressure_num_objects,
int64_t num_objects_per_yield,
optional[c_string] c_tensor_transport):
is_retryable_error[0] = False
worker = ray._private.worker.global_worker
manager = worker.function_actor_manager
cdef:
dict execution_infos = manager.execution_infos
CoreWorker core_worker = worker.core_worker
JobID job_id = core_worker.get_current_job_id()
TaskID task_id = core_worker.get_current_task_id()
task_name = name.decode("utf-8")
title = f"ray::{task_name}"
# Automatically restrict the GPUs (CUDA), neuron_core, TPU accelerator
# runtime_ids, OMP_NUM_THREADS to restrict availability to this task.
# Once actor is created, users can change the visible accelerator ids within
# an actor task and we don't want to reset it.
if (<int>task_type != <int>TASK_TYPE_ACTOR_TASK):
original_visible_accelerator_env_vars = ray._private.utils.set_visible_accelerator_ids()
omp_num_threads_overriden = ray._private.utils.set_omp_num_threads_if_unset()
else:
original_visible_accelerator_env_vars = None
omp_num_threads_overriden = False
# Initialize the actor if this is an actor creation task. We do this here
# before setting the current task ID so that we can get the execution info,
# in case executing the main task throws an exception.
function_descriptor = CFunctionDescriptorToPython(
ray_function.GetFunctionDescriptor())
if <int>task_type == <int>TASK_TYPE_ACTOR_CREATION_TASK:
actor_class = manager.load_actor_class(job_id, function_descriptor)
actor_id = core_worker.get_actor_id()
actor = actor_class.__new__(actor_class)
worker.actors[actor_id] = actor
# Record the actor class via :actor_name: magic token in the log.
#
# (Phase 1): this covers code run before __init__ finishes.
# We need to handle this separately because `__repr__` may not be
# runnable until after `__init__` (e.g., if it accesses fields
# defined in the constructor).
actor_magic_token = "{}{}\n".format(
ray_constants.LOG_PREFIX_ACTOR_NAME, actor_class.__name__)
# Flush to both .out and .err
print(actor_magic_token, end="")
print(actor_magic_token, file=sys.stderr, end="")
# Initial eventloops for asyncio for this actor.
if core_worker.current_actor_is_asyncio():
core_worker.initialize_eventloops_for_actor_concurrency_group(
c_defined_concurrency_groups)
execution_info = execution_infos.get(function_descriptor)
if not execution_info:
execution_info = manager.get_execution_info(
job_id, function_descriptor)
execution_infos[function_descriptor] = execution_info
global current_task_id
try:
task_id = (ray._private.worker.
global_worker.core_worker.get_current_task_id())
# Set the current task ID, which is checked by a separate thread during
# task cancellation. We must do this inside the try block so that, if
# the task is interrupted because of cancellation, we will catch the
# interrupt error here.
with current_task_id_lock:
current_task_id = task_id
execute_task(caller_address,
task_type,
name,
ray_function,
c_resources,
c_args,
c_arg_refs,
debugger_breakpoint,
serialized_retry_exception_allowlist,
returns,
dynamic_returns,
streaming_generator_returns,
is_retryable_error,
actor_repr_name,
application_error,
c_defined_concurrency_groups,
c_name_of_concurrency_group_to_execute,
is_reattempt, execution_info, title, task_name,
is_streaming_generator,
should_retry_exceptions,
generator_backpressure_num_objects,
num_objects_per_yield,
c_tensor_transport)
# Check for cancellation.
PyErr_CheckSignals()
except KeyboardInterrupt as e:
# Catch and handle task cancellation, which will result in an interrupt being
# raised.
e = TaskCancelledError(
core_worker.get_current_task_id()).with_traceback(e.__traceback__)
actor = None
actor_id = core_worker.get_actor_id()
if not actor_id.is_nil():
actor = worker.actors[actor_id]
store_task_errors(
worker, e,
# Task cancellation can happen anytime so we don't really need
# to differentiate between mid-task or not.
False, # task_exception
actor,
actor_id,
execution_info.function_name,
task_type, title, caller_address,
returns,
# application_error: we are passing NULL since we don't want the
# cancel tasks to fail.
NULL)
finally:
with current_task_id_lock:
current_task_id = None
if (<int>task_type == <int>TASK_TYPE_NORMAL_TASK):
if original_visible_accelerator_env_vars:
# Reset the visible accelerator env vars for normal tasks, since they may be reused.
ray._private.utils.reset_visible_accelerator_env_vars(original_visible_accelerator_env_vars)
if omp_num_threads_overriden:
# Reset the OMP_NUM_THREADS environ if it was set.
os.environ.pop("OMP_NUM_THREADS", None)
if execution_info.max_calls != 0:
# Reset the state of the worker for the next task to execute.
# Increase the task execution counter.
manager.increase_task_counter(function_descriptor)
# If we've reached the max number of executions for this worker, exit.
task_counter = manager.get_task_counter(function_descriptor)
if task_counter == execution_info.max_calls:
raise_sys_exit_with_custom_error_message(
f"Exited because worker reached max_calls={execution_info.max_calls}"
" for this method.")
cdef void free_actor_object_callback(const CObjectID &c_object_id) nogil:
# Expected to be called on the owner process. Will free on the primary copy holder.
with gil:
object_id = c_object_id.Hex().decode()
rdt_manager = ray._private.worker.global_worker.rdt_manager
rdt_manager.queue_or_free_object_primary_copy(object_id)
cdef void set_direct_transport_metadata(const CObjectID &c_object_id, const c_string &c_direct_transport_metadata) nogil:
with gil:
object_id = c_object_id.Hex().decode()
tensor_transport_meta = ray_pickle.loads(c_direct_transport_metadata)
rdt_manager = ray._private.worker.global_worker.rdt_manager
rdt_manager.set_tensor_transport_metadata_and_trigger_queued_operations(object_id, tensor_transport_meta)
cdef shared_ptr[LocalMemoryBuffer] ray_error_to_memory_buf(ray_error):
cdef bytes py_bytes = ray_error.to_bytes()
return make_shared[LocalMemoryBuffer](
<uint8_t*>py_bytes, len(py_bytes), True)
cdef void pygilstate_release(PyGILState_STATE gstate) nogil:
with gil:
PyGILState_Release(gstate)
cdef function[void()] initialize_pygilstate_for_thread() nogil:
"""
This function initializes a C++ thread to make it be considered as a
Python thread from the Python interpreter's perspective, regardless of whether
it is executing Python code or not. This function must be called in a thread
before executing any Ray tasks on that thread.
Returns:
A function that calls `PyGILState_Release` to release the GIL state.
This function should be called in a thread before the thread exits.
Reference: https://docs.python.org/3/c-api/init.html#non-python-created-threads
"""
cdef function[void()] callback
with gil:
gstate = PyGILState_Ensure()
callback = bind(pygilstate_release, ref(gstate))
return callback
cdef CRayStatus task_execution_handler(
const CAddress &caller_address,
CTaskType task_type,
const c_string task_name,
const CRayFunction &ray_function,
const unordered_map[c_string, double] &c_resources,
const c_vector[shared_ptr[CRayObject]] &c_args,
const c_vector[CObjectReference] &c_arg_refs,
const c_string debugger_breakpoint,
const c_string serialized_retry_exception_allowlist,
c_vector[c_pair[CObjectID, shared_ptr[CRayObject]]] *returns,
c_vector[c_pair[CObjectID, shared_ptr[CRayObject]]] *dynamic_returns,
c_vector[c_pair[CObjectID, c_bool]] *streaming_generator_returns,
shared_ptr[LocalMemoryBuffer] &creation_task_exception_pb_bytes,
c_bool *is_retryable_error,
c_string *actor_repr_name,
c_string *application_error,
const c_vector[CConcurrencyGroup] &defined_concurrency_groups,
const c_string name_of_concurrency_group_to_execute,
c_bool is_reattempt,
c_bool is_streaming_generator,
c_bool should_retry_exceptions,
int64_t generator_backpressure_num_objects,
int64_t num_objects_per_yield,
optional[c_string] c_tensor_transport) nogil:
with gil, disable_client_hook():
# Initialize job_config if it hasn't already.
# Setup system paths configured in job_config.
maybe_initialize_job_config()
try:
try:
# Exceptions, including task cancellation, should be handled
# internal to this call. If it does raise an exception, that
# indicates that there was an internal error.
execute_task_with_cancellation_handler(
caller_address,
task_type, task_name,
ray_function, c_resources,
c_args, c_arg_refs,
debugger_breakpoint,
serialized_retry_exception_allowlist,
returns,
dynamic_returns,
streaming_generator_returns,
is_retryable_error,
actor_repr_name,
application_error,
defined_concurrency_groups,
name_of_concurrency_group_to_execute,
is_reattempt,
is_streaming_generator,
should_retry_exceptions,
generator_backpressure_num_objects,
num_objects_per_yield,
c_tensor_transport)
except Exception as e:
sys_exit = SystemExit()
if isinstance(e, RayActorError) and \
e.actor_init_failed:
traceback_str = str(e)
logger.error("Exception raised "
f"in creation task: {traceback_str}")
creation_task_exception_pb_bytes = ray_error_to_memory_buf(e)
sys_exit.is_creation_task_error = True
sys_exit.init_error_message = (
"Exception raised from an actor init method. "
f"Traceback: {str(e)}")
else:
traceback_str = traceback.format_exc() + (
"An unexpected internal error "
"occurred while the worker "
"was executing a task.")
ray._private.utils.push_error_to_driver(
ray._private.worker.global_worker,
"worker_crash",
traceback_str,
job_id=None)
sys_exit.unexpected_error_traceback = traceback_str
raise sys_exit
except SystemExit as e:
# Tell the core worker to exit as soon as the result objects
# are processed.
if hasattr(e, "is_creation_task_error"):
return CRayStatus.CreationTaskError(e.init_error_message)
elif e.code is not None and e.code == 0:
# This means the system exit was
# normal based on the python convention.
# https://docs.python.org/3/library/sys.html#sys.exit
msg = f"Worker exits with an exit code {e.code}."
if hasattr(e, "ray_terminate_msg"):
msg += (f" {e.ray_terminate_msg}")
return CRayStatus.IntentionalSystemExit(msg)
else:
msg = f"Worker exits with an exit code {e.code}."
# In K8s, SIGTERM likely means we hit memory limits, so print
# a more informative message there.
if "KUBERNETES_SERVICE_HOST" in os.environ:
msg += (
" The worker may have exceeded K8s pod memory limits.")
if hasattr(e, "ray_terminate_msg"):
msg += (f" {e.ray_terminate_msg}")
if hasattr(e, "unexpected_error_traceback"):
msg += (f" {e.unexpected_error_traceback}")
return CRayStatus.UnexpectedSystemExit(msg)
except Exception as e:
msg = "Unexpected exception raised in task execution handler: {}".format(e)
logger.error(msg)
return CRayStatus.UnexpectedSystemExit(msg)
except BaseException as e:
# Safety net: any BaseException that is not Exception or SystemExit
# (e.g. KeyboardInterrupt, GeneratorExit) would otherwise escape this
# cdef function. Without this, Cython silently returns
# CRayStatus.OK() for unhandled non-Exception/non-SystemExit
# exceptions, causing a CHECK failure in HandleTaskExecutionResult
# when return objects are not populated.
# Convert to UnexpectedSystemExit so the C++ side
# treats this as a clean worker-exiting task failure.
#
# The motivating case is a rapid double `ray.cancel()`. The first
# cancel raises a KeyboardInterrupt that is caught by
# `execute_task_with_cancellation_handler`'s
# `except KeyboardInterrupt` clause, which calls
# `store_task_errors`. If a second cancel arrives while
# `store_task_errors` is running, it queues another SIGINT that
# fires inside the error-storage path. That KeyboardInterrupt
# cannot be re-caught (we are already inside `except
# KeyboardInterrupt`), so it escapes all the way out to this
# handler.
msg = (
"BaseException escaped task execution handlers: "
f"{type(e).__name__}: {e}"
)
logger.error(msg)
return CRayStatus.UnexpectedSystemExit(msg)
return CRayStatus.OK()
cdef c_bool kill_main_task(const CTaskID &task_id) nogil:
with gil:
task_id_to_kill = TaskID(task_id.Binary())
with current_task_id_lock:
if current_task_id != task_id_to_kill:
return False
_thread.interrupt_main()
return True
cdef CRayStatus check_signals() nogil:
with gil:
# The Python exceptions are not handled if it is raised from cdef,
# so we have to handle it here.
try:
if sys.is_finalizing():
return CRayStatus.IntentionalSystemExit(
"Python is exiting.".encode("utf-8")
)
PyErr_CheckSignals()
except KeyboardInterrupt:
return CRayStatus.Interrupted(b"")
except SystemExit as e:
error_msg = (
"SystemExit is raised (sys.exit is called).")
if e.code is not None:
error_msg += f" Exit code: {e.code}."
else:
error_msg += " Exit code was not specified."
if hasattr(e, "ray_terminate_msg"):
error_msg += f" {e.ray_terminate_msg}"
if e.code and e.code == 0:
return CRayStatus.IntentionalSystemExit(error_msg.encode("utf-8"))
else:
return CRayStatus.UnexpectedSystemExit(error_msg.encode("utf-8"))
# By default, if signals raise an exception, Python just prints them.
# To keep the same behavior, we don't handle any other exceptions.
# ray.cancel marks running sync actor tasks canceled without sending an OS
# signal to worker threads (CancelActorTaskOnExecutor for non-async actors).
# Unblock nogil backpressure waits. Uses job/task guards so periodic io threads
# do not call GetCurrentTaskID() without a job (WorkerContext CHECK).
if CCoreWorkerProcess.GetCoreWorker().ShouldInterruptTaskForCancellation():
return CRayStatus.Interrupted(b"")
return CRayStatus.OK()
cdef void gc_collect() nogil:
with gil:
if RayConfig.instance().start_python_gc_manager_thread():
start = time.perf_counter()
worker = ray._private.worker.global_worker
worker.core_worker.trigger_gc()
end = time.perf_counter()
logger.debug("GC event triggered in {} seconds".format(end - start))
else:
start = time.perf_counter()
num_freed = gc.collect()
end = time.perf_counter()
if num_freed > 0:
logger.debug(
"gc.collect() freed {} refs in {} seconds".format(
num_freed, end - start))
cdef c_vector[c_string] spill_objects_handler(
const c_vector[CObjectReference]& object_refs_to_spill) nogil:
cdef:
c_vector[c_string] return_urls
c_vector[c_string] owner_addresses
with gil:
object_refs = VectorToObjectRefs(
object_refs_to_spill,
skip_adding_local_ref=False)
for i in range(object_refs_to_spill.size()):
owner_addresses.push_back(
object_refs_to_spill[i].owner_address()
.SerializeAsString())
try:
with ray._private.worker._changeproctitle(
ray_constants.WORKER_PROCESS_TYPE_SPILL_WORKER,
ray_constants.WORKER_PROCESS_TYPE_SPILL_WORKER_IDLE):
urls = external_storage.spill_objects(
object_refs, owner_addresses)
for url in urls:
return_urls.push_back(url)
except Exception as err:
exception_str = (
"An unexpected internal error occurred while the IO worker "
"was spilling objects: {}".format(err))
logger.exception(exception_str)
ray._private.utils.push_error_to_driver(
ray._private.worker.global_worker,
"spill_objects_error",
traceback.format_exc() + exception_str,
job_id=None)
return return_urls
cdef int64_t restore_spilled_objects_handler(
const c_vector[CObjectReference]& object_refs_to_restore,
const c_vector[c_string]& object_urls) nogil:
cdef:
int64_t bytes_restored = 0
with gil:
urls = []
size = object_urls.size()
for i in range(size):
urls.append(object_urls[i])
object_refs = VectorToObjectRefs(
object_refs_to_restore,
skip_adding_local_ref=False)
try:
with ray._private.worker._changeproctitle(
ray_constants.WORKER_PROCESS_TYPE_RESTORE_WORKER,
ray_constants.WORKER_PROCESS_TYPE_RESTORE_WORKER_IDLE):
bytes_restored = external_storage.restore_spilled_objects(
object_refs, urls)
except Exception:
exception_str = (
"An unexpected internal error occurred while the IO worker "
"was restoring spilled objects.")
logger.exception(exception_str)
if os.getenv("RAY_BACKEND_LOG_LEVEL") == "debug":
ray._private.utils.push_error_to_driver(
ray._private.worker.global_worker,
"restore_objects_error",
traceback.format_exc() + exception_str,
job_id=None)
return bytes_restored
cdef void delete_spilled_objects_handler(
const c_vector[c_string]& object_urls,
CWorkerType worker_type) nogil:
with gil:
urls = []
size = object_urls.size()
for i in range(size):
urls.append(object_urls[i])
try:
# Get proctitle.
if <int> worker_type == <int> WORKER_TYPE_SPILL_WORKER:
original_proctitle = (
ray_constants.WORKER_PROCESS_TYPE_SPILL_WORKER_IDLE)
proctitle = (
ray_constants.WORKER_PROCESS_TYPE_SPILL_WORKER_DELETE)
elif <int> worker_type == <int> WORKER_TYPE_RESTORE_WORKER:
original_proctitle = (
ray_constants.WORKER_PROCESS_TYPE_RESTORE_WORKER_IDLE)
proctitle = (
ray_constants.WORKER_PROCESS_TYPE_RESTORE_WORKER_DELETE)
else:
assert False, ("This line shouldn't be reachable.")
# Delete objects.
with ray._private.worker._changeproctitle(
proctitle,
original_proctitle):
external_storage.delete_spilled_objects(urls)
except Exception:
exception_str = (
"An unexpected internal error occurred while the IO worker "
"was deleting spilled objects.")
logger.exception(exception_str)
ray._private.utils.push_error_to_driver(
ray._private.worker.global_worker,
"delete_spilled_objects_error",
traceback.format_exc() + exception_str,
job_id=None)
cdef c_bool cancel_async_actor_task(const CTaskID &c_task_id) nogil:
"""Attempt to cancel a task running in this asyncio actor.
Returns True if the task was currently running and was cancelled, else False.
Note that the underlying asyncio task may not actually have been cancelled: it
could already have completed or else might not gracefully handle cancellation.
The return value only indicates that the task was found and cancelled.
"""
with gil:
task_id = TaskID(c_task_id.Binary())
worker = ray._private.worker.global_worker
fut = worker.core_worker.get_future_for_running_task(task_id)
if fut is None:
# Either the task hasn't started executing yet or already finished.
return False
fut.cancel()
return True
cdef void unhandled_exception_handler(const CRayObject& error) nogil:
with gil:
worker = ray._private.worker.global_worker
data = None
metadata = None
if error.HasData():
data = Buffer.make(error.GetData())
if error.HasMetadata():
metadata = Buffer.make(error.GetMetadata()).to_pybytes()
# TODO(ekl) why does passing a ObjectRef.nil() lead to shutdown errors?
object_ids = [None]
worker.raise_errors([SerializedRayObject(data, metadata, None)], object_ids)
def maybe_initialize_job_config():
with job_config_initialization_lock:
global job_config_initialized
if job_config_initialized:
return
# Add code search path to sys.path, set load_code_from_local.
core_worker = ray._private.worker.global_worker.core_worker
code_search_path = core_worker.get_job_config().code_search_path
load_code_from_local = False
if code_search_path:
load_code_from_local = True
for p in code_search_path:
if os.path.isfile(p):
p = os.path.dirname(p)
sys.path.insert(0, p)
ray._private.worker.global_worker.set_load_code_from_local(load_code_from_local)
# Add driver's system path to sys.path
py_driver_sys_path = core_worker.get_job_config().py_driver_sys_path
if py_driver_sys_path:
for p in py_driver_sys_path:
sys.path.insert(0, p)
# Cache and set the current job id.
job_id = core_worker.get_current_job_id()
ray._private.worker.global_worker.set_cached_job_id(job_id)
# Record the task name via :task_name: magic token in the log file.
# This is used for the prefix in driver logs `(task_name pid=123) ...`
job_id_magic_token = "{}{}\n".format(
ray_constants.LOG_PREFIX_JOB_ID, job_id.hex())
# Print on both .out and .err
print(job_id_magic_token, end="")
print(job_id_magic_token, file=sys.stderr, end="")
# Configure worker process's Python logging.
serialized_py_logging_config = \
core_worker.get_job_config().serialized_py_logging_config
if serialized_py_logging_config:
logging_config = pickle.loads(serialized_py_logging_config)
try:
logging_config._apply()
except Exception as e:
backtrace = \
"".join(traceback.format_exception(type(e), e, e.__traceback__))
core_worker.drain_and_exit_worker("user", backtrace)
job_config_initialized = True
# This function introduces ~2-7us of overhead per call (i.e., it can be called
# up to hundreds of thousands of times per second).
cdef void get_py_stack(c_string* stack_out) nogil:
"""Get the Python call site.
This can be called from within C++ code to retrieve the file name and line
number of the Python code that is calling into the core worker.
"""
with gil:
try:
frame = inspect.currentframe()
except ValueError: # overhead of exception handling is about 20us
stack_out[0] = "".encode("ascii")
return
msg_frames = []
while frame and len(msg_frames) < 4:
filename = frame.f_code.co_filename
# Decode Ray internal frames to add annotations.
if filename.endswith("_private/worker.py"):
if frame.f_code.co_name == "put":
msg_frames = ["(put object) "]
elif filename.endswith("_private/workers/default_worker.py"):
pass
elif filename.endswith("ray/remote_function.py"):
# TODO(ekl) distinguish between task return objects and
# arguments. This can only be done in the core worker.
msg_frames = ["(task call) "]
elif filename.endswith("ray/actor.py"):
# TODO(ekl) distinguish between actor return objects and
# arguments. This can only be done in the core worker.
msg_frames = ["(actor call) "]
elif filename.endswith("_private/serialization.py"):
if frame.f_code.co_name == "id_deserializer":
msg_frames = ["(deserialize task arg) "]
else:
msg_frames.append("{}:{}:{}".format(
frame.f_code.co_filename, frame.f_code.co_name,
frame.f_lineno))
frame = frame.f_back
stack_out[0] = (ray_constants.CALL_STACK_LINE_DELIMITER
.join(msg_frames).encode("ascii"))
cdef shared_ptr[CBuffer] string_to_buffer(c_string& c_str):
cdef shared_ptr[CBuffer] empty_metadata
if c_str.size() == 0:
return empty_metadata
return dynamic_pointer_cast[
CBuffer, LocalMemoryBuffer](
make_shared[LocalMemoryBuffer](
<uint8_t*>(c_str.data()), c_str.size(), True))
cdef void call_actor_shutdown() noexcept nogil:
"""C++ wrapper function that calls the Python actor shutdown callback."""
with gil:
core_worker = ray._private.worker.global_worker.core_worker
if core_worker.current_actor_is_asyncio():
core_worker.stop_and_join_asyncio_threads_if_exist()
_call_actor_shutdown()
def _call_actor_shutdown():
"""Internal function that calls actor's __ray_shutdown__ method."""
try:
worker = ray._private.worker.global_worker
if not worker.actors:
return
actor_id, actor_instance = next(iter(worker.actors.items()))
if actor_instance is not None:
# Only call __ray_shutdown__ if the method exists and is callable
# This preserves backward compatibility: actors without __ray_shutdown__
# use Python's normal exit flow (including atexit handlers)
if hasattr(actor_instance, '__ray_shutdown__') and callable(getattr(actor_instance, '__ray_shutdown__')):
try:
actor_instance.__ray_shutdown__()
except Exception:
logger.exception("Error during actor __ray_shutdown__ method")
# Always clean up the actor instance
worker.actors.pop(actor_id, None)
except Exception:
# Catch any system-level exceptions to prevent propagation to C++
logger.exception("System error during actor shutdown callback")
cdef class StreamRedirector:
@staticmethod
def redirect_stdout(const c_string &file_path, uint64_t rotation_max_size, uint64_t rotation_max_file_count, c_bool tee_to_stdout, c_bool tee_to_stderr):
cdef CStreamRedirectionOptions opt = CStreamRedirectionOptions()
opt.file_path = file_path
opt.rotation_max_size = rotation_max_size
opt.rotation_max_file_count = rotation_max_file_count
opt.tee_to_stdout = tee_to_stdout
opt.tee_to_stderr = tee_to_stderr
RedirectStdoutOncePerProcess(opt)
@staticmethod
def redirect_stderr(const c_string &file_path, uint64_t rotation_max_size, uint64_t rotation_max_file_count, c_bool tee_to_stdout, c_bool tee_to_stderr):
cdef CStreamRedirectionOptions opt = CStreamRedirectionOptions()
opt.file_path = file_path
opt.rotation_max_size = rotation_max_size
opt.rotation_max_file_count = rotation_max_file_count
opt.tee_to_stdout = tee_to_stdout
opt.tee_to_stderr = tee_to_stderr
RedirectStderrOncePerProcess(opt)
# An empty profile event context to be used when the timeline is disabled.
cdef class EmptyProfileEvent:
def __enter__(self):
pass
def __exit__(self, *args):
pass
cdef class GcsClient:
"""
Client to the GCS server.
This is a thin wrapper around InnerGcsClient with only call frequency collection.
"""
cdef InnerGcsClient inner
def __cinit__(self, address: str,
cluster_id: Optional[str] = None):
# For timeout (DEADLINE_EXCEEDED): retries once with timeout_ms.
#
# For other RpcError (UNAVAILABLE, UNKNOWN): retries indefinitely until it
# thinks GCS is down and kills the whole process.
timeout_ms = RayConfig.instance().py_gcs_connect_timeout_s() * 1000
self.inner = InnerGcsClient.standalone(address, cluster_id, timeout_ms)
def __getattr__(self, name):
# We collect the frequency of each method call.
if "TEST_RAY_COLLECT_KV_FREQUENCY" in os.environ:
with ray._private.utils._CALLED_FREQ_LOCK:
ray._private.utils._CALLED_FREQ[name] += 1
return getattr(self.inner, name)
cdef void _invoke_object_out_of_scope_callback(
const CObjectID &c_object_id, void *user_callback) noexcept nogil:
"""Invoked on the object_freed_callback_service_ thread when an object goes
out of scope. Calls the registered Python callback with the object ID as
``bytes``, then releases the Py_INCREF taken at registration.
Args:
c_object_id: The C++ ObjectID of the object that went out of scope.
user_callback: The Python callable registered by the caller, kept
alive by the Py_INCREF in ``add_object_out_of_scope_callback``.
"""
with gil:
try:
callback = <object>user_callback
id_binary = c_object_id.Binary()
callback(id_binary)
except BaseException:
# Invoked from C++ through a C function pointer, so a propagating
# exception would be undefined behavior; that is why we catch
# everything here, including KeyboardInterrupt/SystemExit.
logger.exception(
"Exception in the callback registered via "
"CoreWorker.add_object_out_of_scope_callback for object %s. The "
"callback must be non-blocking and exception-free, so check it "
"for I/O, blocking calls, or bugs that raise.",
c_object_id.Hex().decode("ascii"),
)
finally:
cpython.Py_DECREF(<object>user_callback)
cdef class CoreWorker:
def __cinit__(self, worker_type, store_socket, raylet_socket,
JobID job_id, GcsClientOptions gcs_options, log_dir,
node_ip_address, node_manager_port,
driver_name,
serialized_job_config, metrics_agent_port, runtime_env_hash,
WorkerID worker_id, session_name, cluster_id, entrypoint,
worker_launch_time_ms, worker_launched_time_ms, debug_source):
cdef CCoreWorkerOptions options = CCoreWorkerOptions()
if worker_type == ray.SCRIPT_MODE:
self.is_driver = True
options.worker_type = WORKER_TYPE_DRIVER
elif worker_type == ray.WORKER_MODE:
self.is_driver = False
options.worker_type = WORKER_TYPE_WORKER
elif worker_type == ray.SPILL_WORKER_MODE:
self.is_driver = False
options.worker_type = WORKER_TYPE_SPILL_WORKER
elif worker_type == ray.RESTORE_WORKER_MODE:
self.is_driver = False
options.worker_type = WORKER_TYPE_RESTORE_WORKER
else:
raise ValueError(f"Unknown worker type: {worker_type}")
options.language = LANGUAGE_PYTHON
options.store_socket = store_socket.encode("ascii")
options.raylet_socket = raylet_socket.encode("ascii")
options.job_id = job_id.native()
options.gcs_options = gcs_options.native()[0]
options.enable_logging = True
options.log_dir = log_dir.encode("utf-8")
options.install_failure_signal_handler = (
not ray_constants.RAY_DISABLE_FAILURE_SIGNAL_HANDLER
)
# https://stackoverflow.com/questions/2356399/tell-if-python-is-in-interactive-mode
options.interactive = hasattr(sys, "ps1")
options.node_ip_address = node_ip_address.encode("utf-8")
options.node_manager_port = node_manager_port
options.driver_name = driver_name
options.initialize_thread_callback = initialize_pygilstate_for_thread
options.task_execution_callback = task_execution_handler
options.free_actor_object_callback = free_actor_object_callback
options.set_direct_transport_metadata = set_direct_transport_metadata
options.check_signals = check_signals
options.gc_collect = gc_collect
options.spill_objects = spill_objects_handler
options.restore_spilled_objects = restore_spilled_objects_handler
options.delete_spilled_objects = delete_spilled_objects_handler
options.unhandled_exception_handler = unhandled_exception_handler
options.cancel_async_actor_task = cancel_async_actor_task
options.get_lang_stack = get_py_stack
options.kill_main = kill_main_task
options.actor_shutdown_callback = call_actor_shutdown
options.serialized_job_config = serialized_job_config
options.metrics_agent_port = metrics_agent_port
options.runtime_env_hash = runtime_env_hash
options.worker_id = worker_id.native()
options.session_name = session_name
options.cluster_id = CClusterID.FromHex(cluster_id)
options.entrypoint = entrypoint
options.worker_launch_time_ms = worker_launch_time_ms
options.worker_launched_time_ms = worker_launched_time_ms
options.debug_source = debug_source
CCoreWorkerProcess.Initialize(options)
self.cgname_to_eventloop_dict = None
self.fd_to_cgname_dict = None
self.eventloop_for_default_cg = None
self.current_runtime_env = None
self._task_id_to_future_lock = threading.Lock()
self._task_id_to_future = {}
self.event_loop_executor = None
self._gc_thread = None
if RayConfig.instance().start_python_gc_manager_thread():
self._gc_thread = PythonGCThread()
self._gc_thread.start()
def shutdown_driver(self):
# If it's a worker, the core worker process should have been
# shutdown. So we can't call
# `CCoreWorkerProcess.GetCoreWorker().GetWorkerType()` here.
# Instead, we use the cached `is_driver` flag to test if it's a
# driver.
assert self.is_driver
if self._gc_thread is not None:
self._gc_thread.stop()
self._gc_thread = None
with nogil:
CCoreWorkerProcess.Shutdown()
def run_task_loop(self):
with nogil:
CCoreWorkerProcess.RunTaskExecutionLoop()
def drain_and_exit_worker(self, exit_type: str, c_string detail):
"""
Exit the current worker process. This API should only be used by
a worker. If this API is called, the worker will wait to finish
currently executing task, initiate the shutdown, and stop
itself gracefully. The given exit_type and detail will be
reported to GCS, and any worker failure error will contain them.
The behavior of this API while a task is running is undefined.
Avoid using the API when a task is still running.
"""
cdef:
CWorkerExitType c_exit_type
cdef const shared_ptr[LocalMemoryBuffer] null_ptr
if exit_type == "user":
c_exit_type = WORKER_EXIT_TYPE_USER_ERROR
elif exit_type == "system":
c_exit_type = WORKER_EXIT_TYPE_SYSTEM_ERROR
elif exit_type == "intentional_system_exit":
c_exit_type = WORKER_EXIT_TYPE_INTENTIONAL_SYSTEM_ERROR
else:
raise ValueError(f"Invalid exit type: {exit_type}")
assert not self.is_driver
with nogil:
CCoreWorkerProcess.GetCoreWorker().Exit(c_exit_type, detail, null_ptr)
def get_current_task_name(self) -> str:
"""Return the current task name.
If it is a normal task, it returns the task name from the main thread.
If it is a threaded actor, it returns the task name for the current thread.
If it is async actor, it returns the task name stored in contextVar for
the current asyncio task.
"""
# We can only obtain the correct task name within asyncio task
# via async_task_name contextvar. We try this first.
# It is needed because the core worker's GetCurrentTask API
# doesn't have asyncio context, thus it cannot return the
# correct task name.
task_name = async_task_name.get()
if task_name is None:
# if it is not within asyncio context, fallback to TaskName
# obtainable from core worker.
task_name = CCoreWorkerProcess.GetCoreWorker().GetCurrentTaskName() \
.decode("utf-8")
return task_name
def get_current_task_function_name(self) -> str:
"""Return the current task function.
If it is a normal task, it returns the task function from the main thread.
If it is a threaded actor, it returns the task function for the current thread.
If it is async actor, it returns the task function stored in contextVar for
the current asyncio task.
"""
# We can only obtain the correct task function within asyncio task
# via async_task_function_name contextvar. We try this first.
# It is needed because the core Worker's GetCurrentTask API
# doesn't have asyncio context, thus it cannot return the
# correct task function.
task_function_name = async_task_function_name.get()
if task_function_name is None:
# if it is not within asyncio context, fallback to TaskName
# obtainable from core worker.
task_function_name = CCoreWorkerProcess.GetCoreWorker() \
.GetCurrentTaskFunctionName().decode("utf-8")
return task_function_name
def get_current_task_id(self) -> TaskID:
"""Return the current task ID.
If it is a normal task, it returns the TaskID from the main thread.
If it is a threaded actor, it returns the TaskID for the current thread.
If it is async actor, it returns the TaskID stored in contextVar for
the current asyncio task.
"""
# We can only obtain the correct task ID within asyncio task
# via async_task_id contextvar. We try this first.
# It is needed because the core Worker's GetCurrentTaskId API
# doesn't have asyncio context, thus it cannot return the
# correct TaskID.
task_id = async_task_id.get()
if task_id is None:
# if it is not within asyncio context, fallback to TaskID
# obtainable from core worker.
task_id = TaskID(
CCoreWorkerProcess.GetCoreWorker().GetCurrentTaskId().Binary())
return task_id
def get_current_task_attempt_number(self):
return CCoreWorkerProcess.GetCoreWorker().GetCurrentTaskAttemptNumber()
def get_task_depth(self):
return CCoreWorkerProcess.GetCoreWorker().GetTaskDepth()
def get_current_job_id(self):
return JobID(
CCoreWorkerProcess.GetCoreWorker().GetCurrentJobId().Binary())
def get_current_node_id(self):
return NodeID(
CCoreWorkerProcess.GetCoreWorker().GetCurrentNodeId().Binary())
def get_actor_id(self):
return ActorID(
CCoreWorkerProcess.GetCoreWorker().GetActorId().Binary())
def get_actor_name(self):
return CCoreWorkerProcess.GetCoreWorker().GetActorName()
def get_placement_group_id(self):
return PlacementGroupID(
CCoreWorkerProcess.GetCoreWorker()
.GetCurrentPlacementGroupId().Binary())
def get_worker_id(self):
return WorkerID(
CCoreWorkerProcess.GetCoreWorker().GetWorkerID().Binary())
def should_capture_child_tasks_in_placement_group(self):
return CCoreWorkerProcess.GetCoreWorker(
).ShouldCaptureChildTasksInPlacementGroup()
def update_task_is_debugger_paused(self, TaskID task_id, is_debugger_paused):
cdef:
CTaskID c_task_id = task_id.native()
return CCoreWorkerProcess.GetCoreWorker(
).UpdateTaskIsDebuggerPaused(c_task_id, is_debugger_paused)
def get_objects(self, object_refs, int64_t timeout_ms=-1):
cdef:
c_vector[shared_ptr[CRayObject]] results
c_vector[CObjectID] c_object_ids = ObjectRefsToVector(object_refs)
with nogil:
op_status = CCoreWorkerProcess.GetCoreWorker().Get(
c_object_ids, timeout_ms, results)
check_status(op_status)
return RayObjectsToSerializedRayObjects(results, object_refs)
def get_if_local(self, object_refs):
"""Get objects from local plasma store directly
without a fetch request to raylet."""
cdef:
c_vector[shared_ptr[CRayObject]] results
c_vector[CObjectID] c_object_ids = ObjectRefsToVector(object_refs)
with nogil:
check_status(
CCoreWorkerProcess.GetCoreWorker().GetIfLocal(
c_object_ids, &results))
return RayObjectsToSerializedRayObjects(results, object_refs)
def object_exists(self, ObjectRef object_ref, memory_store_only=False):
cdef:
c_bool has_object
c_bool is_in_plasma
CObjectID c_object_id = object_ref.native()
with nogil:
check_status(CCoreWorkerProcess.GetCoreWorker().Contains(
c_object_id, &has_object, &is_in_plasma))
return has_object and (not memory_store_only or not is_in_plasma)
cdef unique_ptr[CAddress] _convert_python_address(self, address=None):
""" convert python address to `CAddress`, If not provided,
return nullptr.
Args:
address: worker address.
"""
cdef:
unique_ptr[CAddress] c_address
if address is not None:
c_address = make_unique[CAddress]()
dereference(c_address).ParseFromString(address)
return move(c_address)
def put_file_like_object(
self, metadata, data_size, file_like, ObjectRef object_ref,
owner_address):
"""Directly create a new Plasma Store object from a file like
object. This avoids extra memory copy.
Args:
metadata (bytes): The metadata of the object.
data_size (int): The size of the data buffer.
file_like: A python file object that provides the `readinto`
interface.
object_ref: The new ObjectRef.
owner_address: Owner address for this object ref.
"""
cdef:
CObjectID c_object_id = object_ref.native()
shared_ptr[CBuffer] data_buf
shared_ptr[CBuffer] metadata_buf
unique_ptr[CAddress] c_owner_address = self._convert_python_address(
object_ref.owner_address())
# TODO(suquark): This method does not support put objects to
# in memory store currently.
metadata_buf = string_to_buffer(metadata)
status = CCoreWorkerProcess.GetCoreWorker().CreateExisting(
metadata_buf, data_size, object_ref.native(),
dereference(c_owner_address), &data_buf,
False)
if not status.ok():
logger.debug("Error putting restored object into plasma.")
return
if data_buf == NULL:
logger.debug("Object already exists in 'put_file_like_object'.")
return
data = Buffer.make(data_buf)
view = memoryview(data)
index = 0
while index < data_size:
bytes_read = file_like.readinto(view[index:])
index += bytes_read
with nogil:
# Using custom object refs is not supported because we
# can't track their lifecycle, so we don't pin the object
# in this case.
check_status(
CCoreWorkerProcess.GetCoreWorker().SealExisting(
c_object_id, pin_object=False,
generator_id=CObjectID.Nil(),
owner_address=c_owner_address))
def experimental_channel_put_serialized(self, serialized_object,
ObjectRef object_ref,
num_readers,
timeout_ms):
cdef:
CObjectID c_object_id = object_ref.native()
shared_ptr[CBuffer] data
uint64_t data_size = serialized_object.total_bytes
int64_t c_num_readers = num_readers
int64_t c_timeout_ms = timeout_ms
metadata = string_to_buffer(serialized_object.metadata)
with nogil:
check_status(CCoreWorkerProcess.GetCoreWorker()
.ExperimentalChannelWriteAcquire(
c_object_id,
metadata,
data_size,
c_num_readers,
c_timeout_ms,
&data,
))
if data_size > 0:
(<SerializedObject>serialized_object).write_to(
Buffer.make(data))
with nogil:
check_status(CCoreWorkerProcess.GetCoreWorker()
.ExperimentalChannelWriteRelease(
c_object_id,
))
def experimental_channel_set_error(self, ObjectRef object_ref):
cdef:
CObjectID c_object_id = object_ref.native()
CRayStatus status
with nogil:
status = (CCoreWorkerProcess.GetCoreWorker()
.ExperimentalChannelSetError(c_object_id))
return status.ok()
def experimental_channel_register_writer(self,
ObjectRef writer_ref,
remote_reader_ref_info):
cdef:
CObjectID c_writer_ref = writer_ref.native()
c_vector[CNodeID] c_remote_reader_nodes
c_vector[CReaderRefInfo] c_remote_reader_ref_info
CReaderRefInfo c_reader_ref_info
for node_id, reader_ref_info in remote_reader_ref_info.items():
c_reader_ref_info = CReaderRefInfo()
c_reader_ref_info.reader_ref_id = (
<ObjectRef>reader_ref_info.reader_ref).native()
c_reader_ref_info.owner_reader_actor_id = (
<ActorID>reader_ref_info.ref_owner_actor_id).native()
num_reader_actors = reader_ref_info.num_reader_actors
assert num_reader_actors != 0
c_reader_ref_info.num_reader_actors = num_reader_actors
c_remote_reader_ref_info.push_back(c_reader_ref_info)
c_remote_reader_nodes.push_back(CNodeID.FromHex(node_id))
with nogil:
CCoreWorkerProcess.GetCoreWorker().ExperimentalRegisterMutableObjectWriter(
c_writer_ref,
c_remote_reader_nodes,
)
check_status(
CCoreWorkerProcess.GetCoreWorker()
.ExperimentalRegisterMutableObjectReaderRemote(
c_writer_ref,
c_remote_reader_ref_info,
))
def experimental_channel_register_reader(self, ObjectRef object_ref):
cdef:
CObjectID c_object_id = object_ref.native()
with nogil:
check_status(
CCoreWorkerProcess.GetCoreWorker()
.ExperimentalRegisterMutableObjectReader(c_object_id))
def put_object(
self,
serialized_object,
*,
c_bool pin_object,
c_bool inline_small_object,
c_bool _is_experimental_channel,
tensor_transport: Optional[str] = None,
):
"""Create an object reference with the current worker as the owner.
"""
cdef:
optional[c_string] c_tensor_transport = NULL_TENSOR_TRANSPORT
c_string c_tensor_transport_str
if tensor_transport is not None:
c_tensor_transport_str = tensor_transport.encode()
c_tensor_transport.emplace(move(c_tensor_transport_str))
created_object = self.put_serialized_object_and_increment_local_ref(
serialized_object, c_tensor_transport, pin_object, inline_small_object, _is_experimental_channel)
owner_address = CCoreWorkerProcess.GetCoreWorker().GetRpcAddress().SerializeAsString()
# skip_adding_local_ref is True because it's already added through the call to
# put_serialized_object_and_increment_local_ref.
return ObjectRef(
created_object,
owner_address,
skip_adding_local_ref=True,
tensor_transport=tensor_transport
)
cdef put_serialized_object_and_increment_local_ref(
self,
serialized_object,
optional[c_string] c_tensor_transport,
c_bool pin_object=True,
c_bool inline_small_object=True,
c_bool _is_experimental_channel=False,
):
cdef:
CObjectID c_object_id
shared_ptr[CBuffer] data
shared_ptr[CBuffer] metadata = string_to_buffer(
serialized_object.metadata)
c_vector[CObjectID] contained_object_ids = ObjectRefsToVector(
serialized_object.contained_object_refs)
size_t total_bytes = serialized_object.total_bytes
with nogil:
check_status(CCoreWorkerProcess.GetCoreWorker()
.CreateOwnedAndIncrementLocalRef(
_is_experimental_channel,
metadata,
total_bytes,
contained_object_ids,
&c_object_id,
&data,
inline_small_object,
c_tensor_transport))
if (data.get() == NULL):
# Object already exists
return c_object_id.Binary()
logger.debug(
f"Serialized object size of {c_object_id.Hex()} is {total_bytes} bytes")
if total_bytes > 0:
(<SerializedObject>serialized_object).write_to(
Buffer.make(data))
with nogil:
check_status(
CCoreWorkerProcess.GetCoreWorker().SealOwned(
c_object_id,
pin_object))
return c_object_id.Binary()
def wait(self,
object_refs_or_generators,
int num_returns,
int64_t timeout_ms,
c_bool fetch_local):
cdef:
c_vector[CObjectID] wait_ids
c_vector[c_bool] results
for ref_or_generator in object_refs_or_generators:
if isinstance(ref_or_generator, ObjectRef):
wait_ids.push_back((<ObjectRef>ref_or_generator).native())
elif isinstance(ref_or_generator, ObjectRefGenerator):
wait_ids.push_back(
CObjectID.FromBinary(
ref_or_generator._get_next_object_id_binary()))
else:
raise TypeError(
"wait() expected a list of ray.ObjectRef "
"or ObjectRefGenerator, "
f"got list containing {type(ref_or_generator)}"
)
with nogil:
op_status = CCoreWorkerProcess.GetCoreWorker().Wait(
wait_ids, num_returns, timeout_ms, &results, fetch_local)
check_status(op_status)
assert len(results) == len(object_refs_or_generators)
ready, not_ready = [], []
for i, object_ref_or_generator in enumerate(object_refs_or_generators):
if results[i]:
ready.append(object_ref_or_generator)
else:
not_ready.append(object_ref_or_generator)
return ready, not_ready
def free_objects(self, object_refs, c_bool local_only):
cdef:
c_vector[CObjectID] free_ids = ObjectRefsToVector(object_refs)
with nogil:
check_status(CCoreWorkerProcess.GetCoreWorker().
Delete(free_ids, local_only))
def get_local_ongoing_lineage_reconstruction_tasks(self):
cdef:
unordered_map[CLineageReconstructionTask, uint64_t] tasks
unordered_map[CLineageReconstructionTask, uint64_t].iterator it
with nogil:
tasks = (CCoreWorkerProcess.GetCoreWorker().
GetLocalOngoingLineageReconstructionTasks())
result = []
it = tasks.begin()
while it != tasks.end():
task = common_pb2.LineageReconstructionTask()
task.ParseFromString(dereference(it).first.SerializeAsString())
result.append((task, dereference(it).second))
postincrement(it)
return result
def get_local_object_locations(self, object_refs):
cdef:
c_vector[optional[CObjectLocation]] results
c_vector[CObjectID] lookup_ids = ObjectRefsToVector(object_refs)
with nogil:
check_status(
CCoreWorkerProcess.GetCoreWorker().GetLocalObjectLocations(
lookup_ids, &results))
object_locations = {}
for i in range(results.size()):
# core_worker will return a nullptr for objects that couldn't be
# located
if not results[i].has_value():
continue
else:
object_locations[object_refs[i]] = \
CObjectLocationPtrToDict(&results[i].value())
return object_locations
def get_object_locations(self, object_refs, int64_t timeout_ms):
cdef:
c_vector[shared_ptr[CObjectLocation]] results
c_vector[CObjectID] lookup_ids = ObjectRefsToVector(object_refs)
with nogil:
check_status(
CCoreWorkerProcess.GetCoreWorker().GetLocationFromOwner(
lookup_ids, timeout_ms, &results))
object_locations = {}
for i in range(results.size()):
# core_worker will return a nullptr for objects that couldn't be
# located
if not results[i].get():
continue
else:
object_locations[object_refs[i]] = \
CObjectLocationPtrToDict(results[i].get())
return object_locations
def global_gc(self):
with nogil:
CCoreWorkerProcess.GetCoreWorker().TriggerGlobalGC()
def log_plasma_usage(self):
"""Logs the current usage of the Plasma Store.
Makes an unretriable blocking IPC to the Plasma Store.
Raises an error if cannot connect to the Plasma Store. This should
be fatal for the worker.
"""
cdef:
c_string result
status = CCoreWorkerProcess.GetCoreWorker().GetPlasmaUsage(result)
check_status(status)
logger.warning("Plasma Store Usage:\n{}\n".format(
result.decode("utf-8")))
def get_memory_store_size(self):
return CCoreWorkerProcess.GetCoreWorker().GetMemoryStoreSize()
cdef python_label_match_expressions_to_c(
self, python_expressions,
CLabelMatchExpressions *c_expressions):
cdef:
CLabelMatchExpression* c_expression
CLabelIn * c_label_in
CLabelNotIn * c_label_not_in
for expression in python_expressions:
c_expression = c_expressions[0].add_expressions()
c_expression.set_key(expression.key)
if isinstance(expression.operator, In):
c_label_in = c_expression.mutable_operator_()[0].mutable_label_in()
for value in expression.operator.values:
c_label_in[0].add_values(value)
elif isinstance(expression.operator, NotIn):
c_label_not_in = \
c_expression.mutable_operator_()[0].mutable_label_not_in()
for value in expression.operator.values:
c_label_not_in[0].add_values(value)
elif isinstance(expression.operator, Exists):
c_expression.mutable_operator_()[0].mutable_label_exists()
elif isinstance(expression.operator, DoesNotExist):
c_expression.mutable_operator_()[0].mutable_label_does_not_exist()
cdef python_scheduling_strategy_to_c(
self, python_scheduling_strategy,
CSchedulingStrategy *c_scheduling_strategy):
cdef:
CPlacementGroupSchedulingStrategy \
*c_placement_group_scheduling_strategy
CNodeAffinitySchedulingStrategy *c_node_affinity_scheduling_strategy
CNodeLabelSchedulingStrategy *c_node_label_scheduling_strategy
assert python_scheduling_strategy is not None
if python_scheduling_strategy == "DEFAULT":
c_scheduling_strategy[0].mutable_default_scheduling_strategy()
elif python_scheduling_strategy == "SPREAD":
c_scheduling_strategy[0].mutable_spread_scheduling_strategy()
elif isinstance(python_scheduling_strategy,
PlacementGroupSchedulingStrategy):
c_placement_group_scheduling_strategy = \
c_scheduling_strategy[0] \
.mutable_placement_group_scheduling_strategy()
c_placement_group_scheduling_strategy[0].set_placement_group_id(
python_scheduling_strategy
.placement_group.id.binary())
c_placement_group_scheduling_strategy[0] \
.set_placement_group_bundle_index(
python_scheduling_strategy.placement_group_bundle_index)
c_placement_group_scheduling_strategy[0]\
.set_placement_group_capture_child_tasks(
python_scheduling_strategy
.placement_group_capture_child_tasks)
elif isinstance(python_scheduling_strategy,
NodeAffinitySchedulingStrategy):
c_node_affinity_scheduling_strategy = \
c_scheduling_strategy[0] \
.mutable_node_affinity_scheduling_strategy()
c_node_affinity_scheduling_strategy[0].set_node_id(
NodeID.from_hex(python_scheduling_strategy.node_id).binary())
c_node_affinity_scheduling_strategy[0].set_soft(
python_scheduling_strategy.soft)
c_node_affinity_scheduling_strategy[0].set_spill_on_unavailable(
python_scheduling_strategy._spill_on_unavailable)
c_node_affinity_scheduling_strategy[0].set_fail_on_unavailable(
python_scheduling_strategy._fail_on_unavailable)
elif isinstance(python_scheduling_strategy,
NodeLabelSchedulingStrategy):
c_node_label_scheduling_strategy = \
c_scheduling_strategy[0] \
.mutable_node_label_scheduling_strategy()
self.python_label_match_expressions_to_c(
python_scheduling_strategy.hard,
c_node_label_scheduling_strategy[0].mutable_hard())
self.python_label_match_expressions_to_c(
python_scheduling_strategy.soft,
c_node_label_scheduling_strategy[0].mutable_soft())
else:
raise ValueError(
f"Invalid scheduling_strategy value "
f"{python_scheduling_strategy}. "
f"Valid values are [\"DEFAULT\""
f" | \"SPREAD\""
f" | PlacementGroupSchedulingStrategy"
f" | NodeAffinitySchedulingStrategy]")
def submit_task(self,
Language language,
FunctionDescriptor function_descriptor,
args,
c_string name,
int num_returns,
resources,
int max_retries,
c_bool retry_exceptions,
retry_exception_allowlist,
scheduling_strategy,
c_string debugger_breakpoint,
c_string serialized_runtime_env_info,
int64_t generator_backpressure_num_objects,
int64_t num_objects_per_yield,
c_bool enable_task_events,
labels,
label_selector,
fallback_strategy):
cdef:
unordered_map[c_string, double] c_resources
unordered_map[c_string, c_string] c_labels
CLabelSelector c_label_selector
c_vector[CFallbackOption] c_fallback_strategy
CRayFunction ray_function
CTaskOptions task_options
c_vector[unique_ptr[CTaskArg]] args_vector
c_vector[CObjectReference] return_refs
CSchedulingStrategy c_scheduling_strategy
c_vector[CObjectID] incremented_put_arg_ids
c_string serialized_retry_exception_allowlist
CTaskID current_c_task_id
TaskID current_task = self.get_current_task_id()
c_string call_site
self.python_scheduling_strategy_to_c(
scheduling_strategy, &c_scheduling_strategy)
serialized_retry_exception_allowlist = serialize_retry_exception_allowlist(
retry_exception_allowlist,
function_descriptor)
if RayConfig.instance().record_task_actor_creation_sites():
# TODO(ryw): unify with get_py_stack used by record_ref_creation_sites.
call_site = ''.join(traceback.format_stack())
with self.profile_event(b"submit_task"):
prepare_resources(resources, &c_resources)
prepare_labels(labels, &c_labels)
prepare_label_selector(label_selector, &c_label_selector)
prepare_fallback_strategy(fallback_strategy, &c_fallback_strategy)
ray_function = CRayFunction(
language.lang, function_descriptor.descriptor)
prepare_args_and_increment_put_refs(
language, args, &args_vector, function_descriptor,
&incremented_put_arg_ids)
task_options = CTaskOptions(
name, num_returns, c_resources,
b"",
generator_backpressure_num_objects,
num_objects_per_yield,
serialized_runtime_env_info,
enable_task_events,
c_labels,
c_label_selector,
# `tensor_transport` is currently only supported in Ray Actor tasks.
NULL_TENSOR_TRANSPORT,
c_fallback_strategy)
current_c_task_id = current_task.native()
with nogil:
return_refs = CCoreWorkerProcess.GetCoreWorker().SubmitTask(
ray_function, args_vector, task_options,
max_retries, retry_exceptions,
c_scheduling_strategy,
debugger_breakpoint,
serialized_retry_exception_allowlist,
call_site,
current_c_task_id,
)
# These arguments were serialized and put into the local object
# store during task submission. The backend increments their local
# ref count initially to ensure that they remain in scope until we
# add to their submitted task ref count. Now that the task has
# been submitted, it's safe to remove the initial local ref.
for put_arg_id in incremented_put_arg_ids:
CCoreWorkerProcess.GetCoreWorker().RemoveLocalReference(
put_arg_id)
# The initial local reference is already acquired internally when
# adding the pending task.
return VectorToObjectRefs(return_refs, skip_adding_local_ref=True)
def create_actor(self,
Language language,
FunctionDescriptor function_descriptor,
args,
int64_t max_restarts,
int64_t max_task_retries,
resources,
placement_resources,
int32_t max_concurrency,
is_detached,
c_string name,
c_string ray_namespace,
c_bool is_asyncio,
c_string extension_data,
c_string serialized_runtime_env_info,
concurrency_groups_dict,
int32_t max_pending_calls,
scheduling_strategy,
c_bool enable_task_events,
labels,
label_selector,
c_bool allow_out_of_order_execution,
c_bool enable_tensor_transport,
fallback_strategy,
int64_t actor_generator_backpressure_num_objects=-1,
):
cdef:
CRayFunction ray_function
c_vector[unique_ptr[CTaskArg]] args_vector
c_vector[c_string] dynamic_worker_options
unordered_map[c_string, double] c_resources
unordered_map[c_string, double] c_placement_resources
CActorID c_actor_id
c_vector[CConcurrencyGroup] c_concurrency_groups
CSchedulingStrategy c_scheduling_strategy
c_vector[CObjectID] incremented_put_arg_ids
optional[c_bool] is_detached_optional = nullopt
unordered_map[c_string, c_string] c_labels
CLabelSelector c_label_selector
c_vector[CFallbackOption] c_fallback_strategy
c_string call_site
self.python_scheduling_strategy_to_c(
scheduling_strategy, &c_scheduling_strategy)
if RayConfig.instance().record_task_actor_creation_sites():
# TODO(ryw): unify with get_py_stack used by record_ref_creation_sites.
call_site = ''.join(traceback.format_stack())
with self.profile_event(b"submit_task"):
prepare_resources(resources, &c_resources)
prepare_resources(placement_resources, &c_placement_resources)
prepare_labels(labels, &c_labels)
prepare_label_selector(label_selector, &c_label_selector)
prepare_fallback_strategy(fallback_strategy, &c_fallback_strategy)
ray_function = CRayFunction(
language.lang, function_descriptor.descriptor)
prepare_args_and_increment_put_refs(
language, args, &args_vector, function_descriptor,
&incremented_put_arg_ids)
prepare_actor_concurrency_groups(
concurrency_groups_dict, &c_concurrency_groups)
if is_detached is not None:
is_detached_optional = make_optional[c_bool](
True if is_detached else False)
with nogil:
status = CCoreWorkerProcess.GetCoreWorker().CreateActor(
ray_function, args_vector,
CActorCreationOptions(
max_restarts, max_task_retries, max_concurrency,
c_resources, c_placement_resources,
dynamic_worker_options, is_detached_optional, name,
ray_namespace,
is_asyncio,
c_scheduling_strategy,
serialized_runtime_env_info,
c_concurrency_groups,
allow_out_of_order_execution,
max_pending_calls,
enable_tensor_transport,
enable_task_events,
c_labels,
c_label_selector,
c_fallback_strategy,
actor_generator_backpressure_num_objects),
extension_data,
call_site,
&c_actor_id,
)
# These arguments were serialized and put into the local object
# store during task submission. The backend increments their local
# ref count initially to ensure that they remain in scope until we
# add to their submitted task ref count. Now that the task has
# been submitted, it's safe to remove the initial local ref.
for put_arg_id in incremented_put_arg_ids:
CCoreWorkerProcess.GetCoreWorker().RemoveLocalReference(
put_arg_id)
check_status(status)
return ActorID(c_actor_id.Binary())
def create_placement_group(
self,
c_string name,
c_vector[unordered_map[c_string, double]] bundles,
c_string strategy,
c_bool is_detached,
soft_target_node_id,
c_vector[unordered_map[c_string, c_string]] bundle_label_selector,
dict topology_strategy):
cdef:
CPlacementGroupID c_placement_group_id
CPlacementStrategy c_strategy
CNodeID c_soft_target_node_id = CNodeID.Nil()
unordered_map[c_string, CPlacementStrategy] c_topology_strategy
c_strategy = prepare_c_strategy(strategy)
for label, level_strategy in topology_strategy.items():
c_topology_strategy[label] = prepare_c_strategy(level_strategy)
if soft_target_node_id is not None:
c_soft_target_node_id = CNodeID.FromHex(soft_target_node_id)
with nogil:
check_status(
CCoreWorkerProcess.GetCoreWorker().
CreatePlacementGroup(
CPlacementGroupCreationOptions(
name,
c_strategy,
bundles,
is_detached,
c_soft_target_node_id,
bundle_label_selector,
c_topology_strategy),
&c_placement_group_id))
return PlacementGroupID(c_placement_group_id.Binary())
def remove_placement_group(self, PlacementGroupID placement_group_id):
cdef:
CPlacementGroupID c_placement_group_id = \
placement_group_id.native()
with nogil:
check_status(
CCoreWorkerProcess.GetCoreWorker().
RemovePlacementGroup(c_placement_group_id))
def wait_placement_group_ready(self,
PlacementGroupID placement_group_id,
int64_t timeout_seconds):
cdef CRayStatus status
cdef CPlacementGroupID cplacement_group_id = (
CPlacementGroupID.FromBinary(placement_group_id.binary()))
cdef int64_t ctimeout_seconds = timeout_seconds
with nogil:
status = CCoreWorkerProcess.GetCoreWorker() \
.WaitPlacementGroupReady(cplacement_group_id, ctimeout_seconds)
if status.IsNotFound():
raise Exception("Placement group {} does not exist.".format(
placement_group_id))
return status.ok()
def async_wait_placement_group_ready(self, PlacementGroupID placement_group_id,
serialized_object):
cdef CPlacementGroupID cplacement_group_id = (
CPlacementGroupID.FromBinary(placement_group_id.binary()))
cdef CObjectID c_object_id
cdef c_string serialized_object_data = serialized_object.to_bytes()
cdef c_string serialized_object_metadata = serialized_object.metadata
with nogil:
c_object_id = CCoreWorkerProcess.GetCoreWorker() \
.AsyncWaitPlacementGroupReady(cplacement_group_id,
serialized_object_data,
serialized_object_metadata)
# skip_adding_local_ref is True because it's already added through the
# call to AsyncWaitPlacementGroupReady.
return ObjectRef(c_object_id.Binary(), skip_adding_local_ref=True)
def submit_actor_task(self,
Language language,
ActorID actor_id,
FunctionDescriptor function_descriptor,
args,
c_string name,
int num_returns,
int max_retries,
c_bool retry_exceptions,
retry_exception_allowlist,
double num_method_cpus,
c_string concurrency_group_name,
int64_t generator_backpressure_num_objects,
int64_t num_objects_per_yield,
c_bool enable_task_events,
tensor_transport: Optional[str],
dict labels=None):
cdef:
CActorID c_actor_id = actor_id.native()
unordered_map[c_string, double] c_resources
CRayFunction ray_function
c_vector[unique_ptr[CTaskArg]] args_vector
c_vector[CObjectReference] return_refs
c_vector[CObjectID] incremented_put_arg_ids
CTaskID current_c_task_id = CTaskID.Nil()
TaskID current_task = self.get_current_task_id()
c_string serialized_retry_exception_allowlist
c_string serialized_runtime_env = b"{}"
unordered_map[c_string, c_string] c_labels
CLabelSelector c_label_selector
c_string call_site
c_vector[CFallbackOption] c_fallback_strategy
optional[c_string] c_tensor_transport = NULL_TENSOR_TRANSPORT
c_string c_tensor_transport_str
if tensor_transport is not None:
c_tensor_transport_str = tensor_transport.encode("utf-8")
c_tensor_transport.emplace(move(c_tensor_transport_str))
serialized_retry_exception_allowlist = serialize_retry_exception_allowlist(
retry_exception_allowlist,
function_descriptor)
if RayConfig.instance().record_task_actor_creation_sites():
call_site = ''.join(traceback.format_stack())
with self.profile_event(b"submit_task"):
if num_method_cpus > 0:
c_resources[b"CPU"] = num_method_cpus
prepare_labels(labels, &c_labels)
ray_function = CRayFunction(
language.lang, function_descriptor.descriptor)
prepare_args_and_increment_put_refs(
language, args, &args_vector, function_descriptor,
&incremented_put_arg_ids)
current_c_task_id = current_task.native()
with nogil:
status = CCoreWorkerProcess.GetCoreWorker().SubmitActorTask(
c_actor_id,
ray_function,
args_vector,
CTaskOptions(
name,
num_returns,
c_resources,
concurrency_group_name,
generator_backpressure_num_objects,
num_objects_per_yield,
serialized_runtime_env,
enable_task_events,
c_labels,
c_label_selector,
c_tensor_transport,
c_fallback_strategy),
max_retries,
retry_exceptions,
serialized_retry_exception_allowlist,
call_site,
return_refs,
current_c_task_id,
)
# These arguments were serialized and put into the local object
# store during task submission. The backend increments their local
# ref count initially to ensure that they remain in scope until we
# add to their submitted task ref count. Now that the task has
# been submitted, it's safe to remove the initial local ref.
for put_arg_id in incremented_put_arg_ids:
CCoreWorkerProcess.GetCoreWorker().RemoveLocalReference(
put_arg_id)
if status.ok():
# The initial local reference is already acquired internally
# when adding the pending task.
return VectorToObjectRefs(return_refs,
skip_adding_local_ref=True)
else:
if status.IsOutOfResource():
actor = self.get_actor_handle(actor_id)
actor_handle = (CCoreWorkerProcess.GetCoreWorker()
.GetActorHandle(c_actor_id))
raise PendingCallsLimitExceeded(
f"The task {function_descriptor.function_name} could not be "
f"submitted to {repr(actor)} because more than"
f" {(dereference(actor_handle).MaxPendingCalls())}"
" tasks are queued on the actor. This limit can be adjusted"
" with the `max_pending_calls` actor option.")
else:
raise Exception(f"Failed to submit task to actor {actor_id} "
f"due to {status.message()}")
def kill_actor(self, ActorID actor_id, c_bool no_restart):
cdef:
CActorID c_actor_id = actor_id.native()
CRayStatus status = CRayStatus.OK()
with nogil:
status = CCoreWorkerProcess.GetCoreWorker().KillActor(
c_actor_id, True, no_restart)
if status.IsNotFound():
raise ActorHandleNotFoundError(status.message().decode())
check_status(status)
def cancel_task(self, ObjectRef object_ref, c_bool force_kill,
c_bool recursive):
cdef:
CObjectID c_object_id = object_ref.native()
CRayStatus status = CRayStatus.OK()
with nogil:
status = CCoreWorkerProcess.GetCoreWorker().CancelTask(
c_object_id, force_kill, recursive)
if status.IsInvalidArgument():
raise ValueError(status.message().decode())
if not status.ok():
raise TypeError(status.message().decode())
def is_canceled(self):
"""Check if the current task has been canceled.
Returns:
True if the current task has been canceled, False otherwise.
"""
cdef:
CTaskID c_task_id
c_bool is_canceled
TaskID task_id
# Get the current task ID
task_id = self.get_current_task_id()
c_task_id = task_id.native()
with nogil:
is_canceled = CCoreWorkerProcess.GetCoreWorker().IsTaskCanceled(c_task_id)
return is_canceled
def resource_ids(self):
cdef:
ResourceMappingType resource_mapping = (
CCoreWorkerProcess.GetCoreWorker().GetResourceIDs())
unordered_map[
c_string, c_vector[pair[int64_t, double]]
].iterator iterator = resource_mapping.begin()
c_vector[pair[int64_t, double]] c_value
resources_dict = {}
while iterator != resource_mapping.end():
key = decode(dereference(iterator).first)
c_value = dereference(iterator).second
ids_and_fractions = []
for i in range(c_value.size()):
ids_and_fractions.append(
(c_value[i].first, c_value[i].second))
resources_dict[key] = ids_and_fractions
postincrement(iterator)
return resources_dict
def profile_event(self, c_string event_type, object extra_data=None):
if RayConfig.instance().enable_timeline():
return ProfileEvent.make(
CCoreWorkerProcess.GetCoreWorker().CreateProfileEvent(
event_type), extra_data)
else:
return EmptyProfileEvent()
def remove_actor_handle_reference(self, ActorID actor_id):
cdef:
CActorID c_actor_id = actor_id.native()
CCoreWorkerProcess.GetCoreWorker().RemoveActorHandleReference(
c_actor_id)
def get_local_actor_state(self, ActorID actor_id):
cdef:
CActorID c_actor_id = actor_id.native()
optional[int] state = nullopt
state = CCoreWorkerProcess.GetCoreWorker().GetLocalActorState(c_actor_id)
if state.has_value():
return state.value()
else:
return None
cdef make_actor_handle(self, ActorHandleSharedPtr c_actor_handle,
c_bool weak_ref):
worker = ray._private.worker.global_worker
worker.check_connected()
manager = worker.function_actor_manager
actor_id = ActorID(dereference(c_actor_handle).GetActorID().Binary())
job_id = JobID(dereference(c_actor_handle).CreationJobID().Binary())
language = Language.from_native(
dereference(c_actor_handle).ActorLanguage())
actor_creation_function_descriptor = CFunctionDescriptorToPython(
dereference(c_actor_handle).ActorCreationTaskFunctionDescriptor())
max_task_retries = dereference(c_actor_handle).MaxTaskRetries()
enable_task_events = dereference(c_actor_handle).EnableTaskEvents()
allow_out_of_order_execution = dereference(c_actor_handle).AllowOutOfOrderExecution()
enable_tensor_transport = dereference(c_actor_handle).EnableTensorTransport()
cdef int64_t actor_generator_bp = dereference(
c_actor_handle
).ActorGeneratorBackpressureNumObjects()
if language == Language.PYTHON:
assert isinstance(actor_creation_function_descriptor,
PythonFunctionDescriptor)
# Load actor_method_cpu from actor handle's extension data.
extension_data = <str>dereference(c_actor_handle).ExtensionData()
if extension_data:
actor_method_cpu = int(extension_data)
else:
actor_method_cpu = 0 # Actor is created by non Python worker.
actor_class = manager.load_actor_class(
job_id, actor_creation_function_descriptor)
method_meta = ray.actor._ActorClassMethodMetadata.create(
actor_class, actor_creation_function_descriptor)
return ray.actor.ActorHandle(language, actor_id, max_task_retries,
enable_task_events,
method_meta.method_is_generator,
method_meta.decorators,
method_meta.signatures,
method_meta.num_returns,
method_meta.max_task_retries,
method_meta.retry_exceptions,
method_meta.generator_backpressure_num_objects, # noqa
method_meta.num_objects_per_yield,
method_meta.enable_task_events,
enable_tensor_transport,
method_meta.method_name_to_tensor_transport,
actor_method_cpu,
actor_creation_function_descriptor,
worker.current_cluster_and_job,
weak_ref=weak_ref,
allow_out_of_order_execution=allow_out_of_order_execution,
actor_generator_backpressure_num_objects=int(
actor_generator_bp
))
else:
return ray.actor.ActorHandle(language, actor_id,
0, # max_task_retries,
True, # enable_task_events
{}, # method is_generator
{}, # method decorators
{}, # method signatures
{}, # method num_returns
{}, # method max_task_retries
{}, # method retry_exceptions
{}, # generator_backpressure_num_objects
{}, # num_objects_per_yield
{}, # enable_task_events
False, # enable_tensor_transport
None, # method_name_to_tensor_transport
0, # actor method cpu
actor_creation_function_descriptor,
worker.current_cluster_and_job,
weak_ref=weak_ref,
allow_out_of_order_execution=allow_out_of_order_execution,
actor_generator_backpressure_num_objects=int(
actor_generator_bp
))
def deserialize_and_register_actor_handle(self, const c_string &bytes,
ObjectRef
outer_object_ref,
c_bool weak_ref):
cdef:
CObjectID c_outer_object_id = (outer_object_ref.native() if
outer_object_ref else
CObjectID.Nil())
c_actor_id = (CCoreWorkerProcess
.GetCoreWorker()
.DeserializeAndRegisterActorHandle(
bytes, c_outer_object_id,
add_local_ref=not weak_ref))
return self.make_actor_handle(
CCoreWorkerProcess.GetCoreWorker().GetActorHandle(c_actor_id),
weak_ref)
def get_named_actor_handle(self, const c_string &name,
const c_string &ray_namespace):
cdef:
pair[ActorHandleSharedPtr, CRayStatus] named_actor_handle_pair
# We need it because GetNamedActorHandle needs
# to call a method that holds the gil.
with nogil:
named_actor_handle_pair = (
CCoreWorkerProcess.GetCoreWorker().GetNamedActorHandle(
name, ray_namespace))
check_status(named_actor_handle_pair.second)
return self.make_actor_handle(named_actor_handle_pair.first,
weak_ref=True)
def get_actor_handle(self, ActorID actor_id):
cdef:
CActorID c_actor_id = actor_id.native()
return self.make_actor_handle(
CCoreWorkerProcess.GetCoreWorker().GetActorHandle(c_actor_id),
weak_ref=True)
def list_named_actors(self, c_bool all_namespaces):
"""Returns (namespace, name) for named actors in the system.
If all_namespaces is True, returns all actors in all namespaces,
else returns only the actors in the current namespace.
"""
cdef:
pair[c_vector[pair[c_string, c_string]], CRayStatus] result_pair
with nogil:
result_pair = CCoreWorkerProcess.GetCoreWorker().ListNamedActors(
all_namespaces)
check_status(result_pair.second)
return [
(namespace.decode("utf-8"),
name.decode("utf-8")) for namespace, name in result_pair.first]
def serialize_actor_handle(self, ActorID actor_id):
cdef:
c_string output
CObjectID c_actor_handle_id
check_status(CCoreWorkerProcess.GetCoreWorker().SerializeActorHandle(
actor_id.native(), &output, &c_actor_handle_id))
return output, ObjectRef(c_actor_handle_id.Binary())
def add_object_ref_reference(self, ObjectRef object_ref):
# Note: faster to not release GIL for short-running op.
CCoreWorkerProcess.GetCoreWorker().AddLocalReference(
object_ref.native())
def remove_object_ref_reference(self, ObjectRef object_ref):
cdef:
CObjectID c_object_id = object_ref.native()
# We need to release the gil since object destruction may call the
# unhandled exception handler.
with nogil:
CCoreWorkerProcess.GetCoreWorker().RemoveLocalReference(
c_object_id)
def add_object_out_of_scope_callback(
self, ObjectRef object_ref, callback: Callable[[bytes], None]):
"""Register a Python callable to fire when object_ref goes out of scope.
.. warning::
This is an internal Ray API. Do not use it outside of Ray libraries.
Can only be called on the worker that owns object_ref. Raises
ValueError if object_ref is not owned by this worker.
The callback runs on a dedicated background thread concurrent with the
main Python thread. It must be thread-safe; use a lock if it ever accesses
state shared with the main thread.
.. warning::
The callback runs on a single thread shared by every out-of-scope
notification for this worker, so it MUST be O(1) and non-blocking.
Anything that blocks here serializes every subsequent callback on
this worker. Please do not register any hanging/failing operations
here.
If the callback raises, the exception is logged and swallowed so that
subsequent callbacks are not affected.
Args:
object_ref: The owned object to watch.
callback: Called with the object ID as ``bytes`` when the last
reference is released.
Returns:
True if registered; False if the object is already out of scope
(the callback will never fire).
"""
if not callable(callback):
raise TypeError(
f"callback must be callable, got {type(callback).__name__!r}"
)
cdef CObjectID c_object_id = object_ref.native()
check_status(CCoreWorkerProcess.GetCoreWorker().CheckObjectOwnedByUs(
c_object_id))
cpython.Py_INCREF(callback)
registered = CCoreWorkerProcess.GetCoreWorker() \
.AddObjectOutOfScopeOrFreedCallback(
c_object_id,
_invoke_object_out_of_scope_callback,
<void *>callback)
if not registered:
cpython.Py_DECREF(callback)
return registered
def get_owner_address(self, ObjectRef object_ref):
cdef:
CObjectID c_object_id = object_ref.native()
CAddress c_owner_address
op_status = CCoreWorkerProcess.GetCoreWorker().GetOwnerAddress(
c_object_id, &c_owner_address)
check_status(op_status)
return c_owner_address.SerializeAsString()
def serialize_object_ref(self, ObjectRef object_ref):
cdef:
CObjectID c_object_id = object_ref.native()
CAddress c_owner_address = CAddress()
c_string serialized_object_status
op_status = CCoreWorkerProcess.GetCoreWorker().GetOwnershipInfo(
c_object_id, &c_owner_address, &serialized_object_status)
check_status(op_status)
return (object_ref,
c_owner_address.SerializeAsString(),
serialized_object_status)
def deserialize_and_register_object_ref(
self, const c_string &object_ref_binary,
ObjectRef outer_object_ref,
const c_string &serialized_owner_address,
const c_string &serialized_object_status,
):
cdef:
CObjectID c_object_id = CObjectID.FromBinary(object_ref_binary)
CObjectID c_outer_object_id = (outer_object_ref.native() if
outer_object_ref else
CObjectID.Nil())
CAddress c_owner_address = CAddress()
c_owner_address.ParseFromString(serialized_owner_address)
(CCoreWorkerProcess.GetCoreWorker()
.RegisterOwnershipInfoAndResolveFuture(
c_object_id,
c_outer_object_id,
c_owner_address,
serialized_object_status))
cdef store_task_output(self, serialized_object, const CObjectID &return_id,
const CObjectID &generator_id,
size_t data_size, shared_ptr[CBuffer] &metadata,
const c_vector[CObjectID] &contained_id,
const CAddress &caller_address,
int64_t *task_output_inlined_bytes,
shared_ptr[CRayObject] *return_ptr):
"""Store a task return value in plasma or as an inlined object."""
with nogil:
# For objects that can't be inlined, return_ptr will only be set if
# the object doesn't already exist in plasma.
check_status(
CCoreWorkerProcess.GetCoreWorker().AllocateReturnObject(
return_id, data_size, metadata, contained_id, caller_address,
task_output_inlined_bytes, return_ptr))
if return_ptr.get() != NULL:
if return_ptr.get().HasData():
(<SerializedObject>serialized_object).write_to(
Buffer.make(return_ptr.get().GetData()))
with nogil:
check_status(
CCoreWorkerProcess.GetCoreWorker().SealReturnObject(
return_id, return_ptr[0], generator_id, caller_address))
return True
else:
with nogil:
# Pins the object, succeeds if the object exists in plasma and is
# sealed.
success = (
CCoreWorkerProcess.GetCoreWorker().PinExistingReturnObject(
return_id, return_ptr, generator_id, caller_address))
return success
cdef store_task_outputs(self,
worker, outputs,
const CAddress &caller_address,
c_vector[c_pair[CObjectID, shared_ptr[CRayObject]]]
*returns,
ref_generator_id=None,
optional[c_string] c_tensor_transport=NULL_TENSOR_TRANSPORT):
cdef:
CObjectID return_id
size_t data_size
shared_ptr[CBuffer] metadata
c_vector[CObjectID] contained_id
int64_t task_output_inlined_bytes
int64_t num_returns = -1
CObjectID c_ref_generator_id = CObjectID.Nil()
shared_ptr[CRayObject] *return_ptr
c_string c_pickled_rdt_metadata
if ref_generator_id:
c_ref_generator_id = CObjectID.FromBinary(ref_generator_id)
num_outputs_stored = 0
if not c_ref_generator_id.IsNil():
# The task specified a dynamic number of return values. Determine
# the expected number of return values.
if returns[0].size() > 0:
# We are re-executing the task. We should return the same
# number of objects as before.
num_returns = returns[0].size()
else:
# This is the first execution of the task, so we don't know how
# many return objects it should have yet.
# NOTE(swang): returns could also be empty if the task returned
# an empty generator and was re-executed. However, this should
# not happen because we never reconstruct empty
# DynamicObjectRefGenerators (since these aren't stored in plasma).
num_returns = -1
else:
# The task specified how many return values it should have.
num_returns = returns[0].size()
if num_returns == 0:
if outputs is not None and len(outputs) > 0:
# Warn if num_returns=0 but the task returns a non-None value (likely unintended).
task_name = self.get_current_task_name()
obj_value = repr(outputs)
warnings.warn(
f"Task '{task_name}' has num_returns=0 but returned a non-None value '{obj_value}'. "
"The return value will be ignored.",
NumReturnsWarning,
stacklevel=2
)
return num_outputs_stored
tensor_transport = None
if c_tensor_transport.has_value():
tensor_transport = c_tensor_transport.value().decode("utf-8")
task_output_inlined_bytes = 0
i = -1
for i, output in enumerate(outputs):
if num_returns >= 0 and i >= num_returns:
raise ValueError(
"Task returned more than num_returns={} objects.".format(
num_returns))
# TODO(sang): Remove it when the streaming generator is
# enabled by default.
while i >= returns[0].size():
return_id = (CCoreWorkerProcess.GetCoreWorker()
.AllocateDynamicReturnId(
caller_address, CTaskID.Nil(), NULL_PUT_INDEX))
returns[0].push_back(
c_pair[CObjectID, shared_ptr[CRayObject]](
return_id, shared_ptr[CRayObject]()))
assert i < returns[0].size()
return_id = returns[0][i].first
if returns[0][i].second == nullptr:
returns[0][i].second = shared_ptr[CRayObject]()
return_ptr = &returns[0][i].second
# Skip return values that we already created. This can occur if
# there were multiple return values, and we initially errored while
# trying to create one of them.
if (return_ptr.get() != NULL and return_ptr.get().GetData().get()
!= NULL):
continue
context = worker.get_serialization_context()
# TODO(kevin85421): We should consider unifying both serialization logic in the future
# when GPU objects are more stable. We currently separate the logic to ensure
# GPU object-related logic does not affect the normal object serialization logic.
if tensor_transport is not None:
# `output` contains tensors. We need to retrieve these tensors from `output`
# and store them in the RDTManager.
serialized_object, tensors = context.serialize_rdt_objects(output, tensor_transport)
pickled_rdt_metadata = context.store_rdt_objects(
return_id.Hex().decode("ascii"), tensors, tensor_transport)
# One copy from python bytes object to C++ string
c_pickled_rdt_metadata = pickled_rdt_metadata
else:
serialized_object = context.serialize(output)
data_size = serialized_object.total_bytes
metadata_str = serialized_object.metadata
if ray._private.worker.global_worker.debugger_get_breakpoint:
breakpoint = (
ray._private.worker.global_worker.debugger_get_breakpoint)
metadata_str += (
b"," + ray_constants.OBJECT_METADATA_DEBUG_PREFIX +
breakpoint.encode())
# Reset debugging context of this worker.
ray._private.worker.global_worker.debugger_get_breakpoint = b""
metadata = string_to_buffer(metadata_str)
contained_id = ObjectRefsToVector(
serialized_object.contained_object_refs)
# It's possible for store_task_output to fail when the object already
# exists, but we fail to pin it. We can fail to pin the object if
# 1. it exists but isn't sealed yet because it's being written to by
# another worker. We'll keep looping until it's sealed.
# 2. it existed during the allocation attempt but was evicted before
# the pin attempt. We'll allocate and write the second time.
base_backoff_s = 1
attempt = 1
max_attempts = 6 # 6 attempts =~ 60 seconds of total backoff time
while not self.store_task_output(
serialized_object,
return_id,
c_ref_generator_id,
data_size,
metadata,
contained_id,
caller_address,
&task_output_inlined_bytes,
return_ptr):
if (attempt > max_attempts):
raise RaySystemError(
"Failed to store task output with object id {} after {} attempts.".format(
return_id.Hex().decode("ascii"),
max_attempts))
time.sleep(base_backoff_s * (2 ** (attempt-1)))
attempt += 1
continue
if tensor_transport is not None:
return_ptr.get().SetDirectTransportMetadata(move(c_pickled_rdt_metadata))
c_pickled_rdt_metadata = c_string()
num_outputs_stored += 1
i += 1
if i < num_returns:
raise ValueError(
"Task returned {} objects, but num_returns={}.".format(
i, num_returns))
return num_outputs_stored
cdef c_function_descriptors_to_python(
self,
const c_vector[CFunctionDescriptor] &c_function_descriptors):
ret = []
for i in range(c_function_descriptors.size()):
ret.append(CFunctionDescriptorToPython(c_function_descriptors[i]))
return ret
cdef initialize_eventloops_for_actor_concurrency_group(
self,
const c_vector[CConcurrencyGroup] &c_defined_concurrency_groups):
cdef:
CConcurrencyGroup c_concurrency_group
self.cgname_to_eventloop_dict = {}
self.fd_to_cgname_dict = {}
self.eventloop_for_default_cg = get_new_event_loop()
self.thread_for_default_cg = threading.Thread(
target=lambda: self.eventloop_for_default_cg.run_forever(),
name="AsyncIO Thread: default"
)
self.thread_for_default_cg.start()
for i in range(c_defined_concurrency_groups.size()):
c_concurrency_group = c_defined_concurrency_groups[i]
cg_name = c_concurrency_group.GetName().decode("ascii")
function_descriptors = self.c_function_descriptors_to_python(
c_concurrency_group.GetFunctionDescriptors())
async_eventloop = get_new_event_loop()
async_thread = threading.Thread(
target=lambda: async_eventloop.run_forever(),
name="AsyncIO Thread: {}".format(cg_name)
)
async_thread.start()
self.cgname_to_eventloop_dict[cg_name] = {
"eventloop": async_eventloop,
"thread": async_thread,
}
for fd in function_descriptors:
self.fd_to_cgname_dict[fd] = cg_name
def get_event_loop_executor(self) -> concurrent.futures.ThreadPoolExecutor:
if self.event_loop_executor is None:
# NOTE: We're deliberately allocating thread-pool executor with
# a single thread, provided that many of its use-cases are
# not thread-safe yet (for ex, reporting streaming generator output)
self.event_loop_executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)
return self.event_loop_executor
def reset_event_loop_executor(self, executor: concurrent.futures.ThreadPoolExecutor):
self.event_loop_executor = executor
def get_event_loop(self, function_descriptor, specified_cgname):
# __init__ will be invoked in default eventloop
if function_descriptor.function_name == "__init__":
return self.eventloop_for_default_cg, self.thread_for_default_cg
if specified_cgname is not None:
if specified_cgname in self.cgname_to_eventloop_dict:
this_group = self.cgname_to_eventloop_dict[specified_cgname]
return (this_group["eventloop"], this_group["thread"])
if function_descriptor in self.fd_to_cgname_dict:
curr_cgname = self.fd_to_cgname_dict[function_descriptor]
if curr_cgname in self.cgname_to_eventloop_dict:
return (
self.cgname_to_eventloop_dict[curr_cgname]["eventloop"],
self.cgname_to_eventloop_dict[curr_cgname]["thread"])
else:
raise ValueError(
"The function {} is defined to be executed "
"in the concurrency group {} . But there is no this group."
.format(function_descriptor, curr_cgname))
return self.eventloop_for_default_cg, self.thread_for_default_cg
def run_async_func_or_coro_in_event_loop(
self,
func_or_coro: Union[Callable[[Any, Any], Awaitable[Any]], Awaitable],
function_descriptor: FunctionDescriptor,
specified_cgname: str,
*,
task_id: Optional[TaskID] = None,
task_name: Optional[str] = None,
func_args: Optional[Tuple] = None,
func_kwargs: Optional[Dict] = None,
):
"""Run the async function or coroutine to the event loop.
The event loop is running in a separate thread.
Args:
func_or_coro: Async function (not a generator) or awaitable objects.
function_descriptor: The function descriptor.
specified_cgname: The name of a concurrent group.
task_id: The task ID to track the future. If None is provided
the future is not tracked with a task ID.
(e.g., When we deserialize the arguments, we don't want to
track the task_id -> future mapping).
func_args: The arguments for the async function.
func_kwargs: The keyword arguments for the async function.
NOTE: func_args and func_kwargs are intentionally passed as a tuple/dict and
not unpacked to avoid collisions between system arguments and user-provided
arguments. See https://github.com/ray-project/ray/issues/41272.
"""
cdef:
CFiberEvent event
if func_args is None:
func_args = tuple()
if func_kwargs is None:
func_kwargs = dict()
# Increase recursion limit if necessary. In asyncio mode,
# we have many parallel callstacks (represented in fibers)
# that's suspended for execution. Python interpreter will
# mistakenly count each callstack towards recusion limit.
# We don't need to worry about stackoverflow here because
# the max number of callstacks is limited in direct actor
# transport with max_concurrency flag.
increase_recursion_limit()
eventloop, _ = self.get_event_loop(
function_descriptor, specified_cgname)
async def async_func():
try:
if task_id:
async_task_id.set(task_id)
if task_name is not None:
async_task_name.set(task_name)
async_task_function_name.set(function_descriptor.repr)
if inspect.isawaitable(func_or_coro):
coroutine = func_or_coro
else:
coroutine = func_or_coro(*func_args, **func_kwargs)
return await coroutine
finally:
event.Notify()
future = asyncio.run_coroutine_threadsafe(async_func(), eventloop)
if task_id:
with self._task_id_to_future_lock:
self._task_id_to_future[task_id] = future
with nogil:
(CCoreWorkerProcess.GetCoreWorker()
.YieldCurrentFiber(event))
try:
result = future.result()
except concurrent.futures.CancelledError:
raise TaskCancelledError(task_id)
finally:
if task_id:
with self._task_id_to_future_lock:
self._task_id_to_future.pop(task_id)
return result
def stop_and_join_asyncio_threads_if_exist(self):
event_loops = []
threads = []
if self.event_loop_executor:
self.event_loop_executor.shutdown(
wait=True, cancel_futures=True)
if self.eventloop_for_default_cg is not None:
event_loops.append(self.eventloop_for_default_cg)
if self.thread_for_default_cg is not None:
threads.append(self.thread_for_default_cg)
if self.cgname_to_eventloop_dict:
for event_loop_and_thread in self.cgname_to_eventloop_dict.values():
event_loops.append(event_loop_and_thread["eventloop"])
threads.append(event_loop_and_thread["thread"])
for event_loop in event_loops:
event_loop.call_soon_threadsafe(
event_loop.stop)
for thread in threads:
thread.join()
def current_actor_is_asyncio(self):
return (CCoreWorkerProcess.GetCoreWorker().GetWorkerContext()
.CurrentActorIsAsync())
def set_current_actor_should_exit(self):
with exit_actor_task_ids_lock:
exit_actor_task_ids.add(self.get_current_task_id())
return (CCoreWorkerProcess.GetCoreWorker().GetWorkerContext()
.SetCurrentActorShouldExit())
def get_current_actor_should_exit(self):
return (CCoreWorkerProcess.GetCoreWorker().GetWorkerContext()
.GetCurrentActorShouldExit())
def current_actor_max_concurrency(self):
return (CCoreWorkerProcess.GetCoreWorker().GetWorkerContext()
.CurrentActorMaxConcurrency())
def get_current_root_detached_actor_id(self) -> ActorID:
# This is only used in test
return ActorID(CCoreWorkerProcess.GetCoreWorker().GetWorkerContext()
.GetRootDetachedActorID().Binary())
def get_future_for_running_task(self, task_id: Optional[TaskID]) -> Optional[concurrent.futures.Future]:
"""Get the future corresponding to a running task (or None).
The underyling asyncio task might be queued, running, or completed.
"""
with self._task_id_to_future_lock:
return self._task_id_to_future.get(task_id)
def get_current_runtime_env(self) -> str:
# This should never change, so we can safely cache it to avoid ser/de
if self.current_runtime_env is None:
if self.is_driver:
job_config = self.get_job_config()
serialized_env = job_config.runtime_env_info \
.serialized_runtime_env
else:
serialized_env = CCoreWorkerProcess.GetCoreWorker() \
.GetWorkerContext().GetCurrentSerializedRuntimeEnv() \
.decode("utf-8")
self.current_runtime_env = serialized_env
return self.current_runtime_env
def trigger_gc(self):
self._gc_thread.trigger_gc()
def get_pending_children_task_ids(self, parent_task_id: TaskID):
cdef:
CTaskID c_parent_task_id = parent_task_id.native()
c_vector[CTaskID] ret
c_vector[CTaskID].iterator it
result = []
with nogil:
ret = CCoreWorkerProcess.GetCoreWorker().GetPendingChildrenTasks(
c_parent_task_id)
it = ret.begin()
while it != ret.end():
result.append(TaskID(dereference(it).Binary()))
postincrement(it)
return result
def get_all_reference_counts(self):
cdef:
unordered_map[CObjectID, pair[size_t, size_t]] c_ref_counts
unordered_map[CObjectID, pair[size_t, size_t]].iterator it
c_ref_counts = (
CCoreWorkerProcess.GetCoreWorker().GetAllReferenceCounts())
it = c_ref_counts.begin()
ref_counts = {}
while it != c_ref_counts.end():
object_ref = dereference(it).first.Hex()
ref_counts[object_ref] = {
"local": dereference(it).second.first,
"submitted": dereference(it).second.second}
postincrement(it)
return ref_counts
def set_get_async_callback(self, ObjectRef object_ref, user_callback: Callable):
# NOTE: we need to manually increment the Python reference count to avoid the
# callback object being garbage collected before it's called by the core worker.
# This means we *must* guarantee that the ref is manually decremented to avoid
# a leak.
cpython.Py_INCREF(user_callback)
CCoreWorkerProcess.GetCoreWorker().GetAsync(
object_ref.native(),
async_callback,
<void*>user_callback
)
def push_error(self, JobID job_id, error_type, error_message,
double timestamp):
check_status(CCoreWorkerProcess.GetCoreWorker().PushError(
job_id.native(), error_type.encode("utf-8"),
error_message.encode("utf-8"), timestamp))
def get_job_config(self):
cdef CJobConfig c_job_config
# We can cache the deserialized job config object here because
# the job config will not change after a job is submitted.
if self.job_config is None:
c_job_config = CCoreWorkerProcess.GetCoreWorker().GetJobConfig()
self.job_config = common_pb2.JobConfig()
self.job_config.ParseFromString(c_job_config.SerializeAsString())
return self.job_config
def get_local_memory_store_bytes_used(self):
cdef:
int64_t num_bytes_used
with nogil:
num_bytes_used = (
CCoreWorkerProcess.GetCoreWorker().GetLocalMemoryStoreBytesUsed())
return num_bytes_used
def record_task_log_start(
self, task_id: TaskID, int attempt_number,
stdout_path, stderr_path,
int64_t out_start_offset, int64_t err_start_offset):
cdef:
CTaskID c_task_id = task_id.native()
c_string c_stdout_path = stdout_path.encode("utf-8")
c_string c_stderr_path = stderr_path.encode("utf-8")
with nogil:
CCoreWorkerProcess.GetCoreWorker() \
.RecordTaskLogStart(c_task_id, attempt_number,
c_stdout_path, c_stderr_path,
out_start_offset, err_start_offset)
def record_task_log_end(
self, task_id: TaskID, int attempt_number,
int64_t out_end_offset, int64_t err_end_offset):
cdef:
CTaskID c_task_id = task_id.native()
with nogil:
CCoreWorkerProcess.GetCoreWorker() \
.RecordTaskLogEnd(c_task_id, attempt_number,
out_end_offset, err_end_offset)
cdef CObjectID allocate_dynamic_return_id_for_generator(
self,
const CAddress &owner_address,
const CTaskID &task_id,
return_size,
generator_index,
is_async_actor):
"""Allocate a dynamic return ID for a generator task.
NOTE: When is_async_actor is True,
this API SHOULD NOT BE called
within an async actor's event IO thread. The caller MUST ensure
this for correctness. It is due to the limitation WorkerContext
API when async actor is used.
See https://github.com/ray-project/ray/issues/10324 for further details.
Args:
owner_address: The address of the owner (caller) of the
generator task.
task_id: The task ID of the generator task.
return_size: The size of the static return from the task.
generator_index: The index of dynamically generated object
ref.
is_async_actor: True if the allocation is for async actor.
If async actor is used, we should calculate the
put_index ourselves.
"""
# Generator only has 1 static return.
assert return_size == 1
if is_async_actor:
# This part of code has a couple of assumptions.
# - This API is not called within an asyncio event loop
# thread.
# - Ray object ref is generated by incrementing put_index
# whenever a new return value is added or ray.put is called.
#
# When an async actor is used, it uses its own thread to execute
# async tasks. That means all the ray.put will use a put_index
# scoped to a asyncio event loop thread.
# This means the execution thread that this API will be called
# will only create "return" objects. That means if we use
# return_size + genreator_index as a put_index, it is guaranteed
# to be unique.
#
# Why do we need it?
#
# We have to provide a put_index ourselves here because
# the current implementation only has 1 worker context at any
# given time, meaning WorkerContext::TaskID & WorkerContext::PutIndex
# both could be incorrect (duplicated) when this API is called.
return CCoreWorkerProcess.GetCoreWorker().AllocateDynamicReturnId(
owner_address,
task_id,
# Should add 1 because put index is always incremented
# before it is used. So if you have 1 return object
# the next index will be 2.
make_optional[ObjectIDIndexType](
<int>1 + <int>return_size + <int>generator_index) # put_index
)
else:
return CCoreWorkerProcess.GetCoreWorker().AllocateDynamicReturnId(
owner_address,
task_id,
make_optional[ObjectIDIndexType](
<int>1 + <int>return_size + <int>generator_index))
def async_delete_object_ref_stream(self, ObjectRef generator_id):
cdef:
CObjectID c_generator_id = generator_id.native()
with nogil:
CCoreWorkerProcess.GetCoreWorker().AsyncDelObjectRefStream(c_generator_id)
def try_read_next_object_ref_stream(self, ObjectRef generator_id):
cdef:
CObjectID c_generator_id = generator_id.native()
CObjectReference c_object_ref
with nogil:
check_status(
CCoreWorkerProcess.GetCoreWorker().TryReadObjectRefStream(
c_generator_id, &c_object_ref))
return ObjectRef(
c_object_ref.object_id(),
c_object_ref.owner_address().SerializeAsString(),
"",
# Already added when the ref is updated.
skip_adding_local_ref=True)
def try_read_next_object_ref_stream_n(
self, ObjectRef generator_id, int64_t num_items):
"""
Advance the ObjectRefStream cursor by num_items.
Args:
generator_id: The object ref id of the streaming generator task.
num_items: The number of indexes to advance past, starting from
the current head of the stream.
"""
cdef:
CObjectID c_generator_id = generator_id.native()
if num_items <= 0:
raise ValueError("num_items must be positive")
with nogil:
check_status(
CCoreWorkerProcess.GetCoreWorker().TryReadObjectRefStreamN(
c_generator_id, num_items))
def is_object_ref_stream_finished(self, ObjectRef generator_id):
cdef:
CObjectID c_generator_id = generator_id.native()
c_bool finished
with nogil:
finished = CCoreWorkerProcess.GetCoreWorker().StreamingGeneratorIsFinished(
c_generator_id)
return finished
def peek_object_ref_stream(self, ObjectRef generator_id):
cdef:
CObjectID c_generator_id = generator_id.native()
pair[CObjectReference, c_bool] c_object_ref_and_is_ready_pair
with nogil:
c_object_ref_and_is_ready_pair = (
CCoreWorkerProcess.GetCoreWorker().PeekObjectRefStream(
c_generator_id))
return (ObjectRef(
c_object_ref_and_is_ready_pair.first.object_id(),
c_object_ref_and_is_ready_pair.first.owner_address().SerializeAsString()), # noqa
c_object_ref_and_is_ready_pair.second)
def peek_object_ref_stream_n(self, ObjectRef generator_id, int64_t num_items):
"""
Read multiple next indexes of an ObjectRefStream of generator_id without
consuming them.
Args:
generator_id: The object ref id of the streaming generator task.
num_items: Number of next refs to peek.
Returns:
Object references for the next indexes and whether each object is
ready.
"""
cdef:
CObjectID c_generator_id = generator_id.native()
c_vector[pair[CObjectReference, c_bool]] c_object_refs_and_ready
CObjectReference c_object_ref
if num_items <= 0:
raise ValueError("num_items must be positive")
with nogil:
c_object_refs_and_ready = (
CCoreWorkerProcess.GetCoreWorker().PeekObjectRefStreamN(
c_generator_id, num_items))
refs_and_ready = []
for i in range(c_object_refs_and_ready.size()):
c_object_ref = c_object_refs_and_ready[i].first
refs_and_ready.append(
(ObjectRef(
c_object_ref.object_id(),
c_object_ref.owner_address().SerializeAsString()),
c_object_refs_and_ready[i].second))
return refs_and_ready
def peek_next_object_id_binary(self, ObjectRef generator_id):
"""Return the binary form of the next object id in the stream."""
cdef:
CObjectID c_generator_id = generator_id.native()
CObjectID c_next_object_id
with nogil:
c_next_object_id = (
CCoreWorkerProcess.GetCoreWorker().PeekObjectIdStream(
c_generator_id))
return c_next_object_id.Binary()
cdef void async_callback(shared_ptr[CRayObject] obj,
CObjectID object_ref,
void *user_callback_ptr) with gil:
cdef:
c_vector[shared_ptr[CRayObject]] objects_to_deserialize
try:
# Object is retrieved from in memory store.
# Here we go through the code path used to deserialize objects.
objects_to_deserialize.push_back(obj)
serialized_ray_objects = RayObjectsToSerializedRayObjects(
objects_to_deserialize)
ids_to_deserialize = [ObjectRef(object_ref.Binary())]
result = ray._private.worker.global_worker.deserialize_objects(
serialized_ray_objects, ids_to_deserialize)[0]
user_callback = <object>user_callback_ptr
user_callback(result)
except Exception:
# Only log the error here because this callback is called from Cpp
# and Cython will ignore the exception anyway
logger.exception("failed to run async callback (user func)")
finally:
# NOTE: we manually increment the Python reference count of the callback when
# registering it in the core worker, so we must decrement here to avoid a leak.
cpython.Py_DECREF(user_callback)
# Note this deletes keys with prefix `RAY{key_prefix}@`
# Example: with key_prefix = `default`, we remove all `RAYdefault@...` keys.
def del_key_prefix_from_storage(host, port, username, password, use_ssl, key_prefix):
return RedisDelKeyPrefixSync(host, port, username, password, use_ssl, key_prefix)
def get_session_key_from_storage(host, port, username, password, use_ssl, config, key):
"""
Get the session key from the storage.
Intended to be used for session_name only.
Args:
host: The address of the owner (caller) of the
generator task.
port: The task ID of the generator task.
username: The Redis username.
password: The Redis password.
use_ssl: Whether to use SSL.
config: The Ray config. Used to get storage namespace.
key: The key to retrieve.
"""
cdef:
c_string data
result = RedisGetKeySync(
host, port, username, password, use_ssl, config, key, &data)
if result:
return data
else:
logger.info("Could not retrieve session key from storage.")
return None