Files
2026-07-13 13:17:40 +08:00

240 lines
7.8 KiB
Cython

"""Cython bindings for RayEventRecorder.
This module provides Python access to the C++ RayEventRecorder for emitting
internal Ray events from Python code (e.g., submission job events).
"""
from ray.includes.event_recorder cimport (
CRayEventInterface,
CPythonEventRecorder,
CreatePythonRayEvent,
)
from ray.includes.common cimport move
from libc.stdint cimport int64_t
from libcpp.memory cimport unique_ptr
from libcpp.vector cimport vector as c_vector
from libcpp.string cimport string as c_string
import logging
import threading
logger = logging.getLogger(__name__)
cdef class RayEvent:
"""Python wrapper holding event data for transfer to C++.
This class stores event metadata and serialized protobuf data. When added
to the EventRecorder, it creates the underlying C++ RayEvent object.
Args:
source_type: Integer value of RayEvent.SourceType enum.
event_type: Integer value of RayEvent.EventType enum.
severity: Integer value of RayEvent.Severity enum.
entity_id: Unique identifier for the event entity (e.g., submission_id).
message: Optional message associated with the event.
session_name: The Ray session name.
serialized_data: Serialized protobuf bytes of the nested event message.
nested_event_field_number: The field number in RayEvent proto for the
nested event message. Use RayEventProto.<FIELD>_FIELD_NUMBER constants.
event_id: Optional explicit event id bytes. Pass empty bytes to let the
C++ layer generate a random id (matching the convention used by the
other RayEventInterface subclasses). A non-empty value is preserved
as-is — use this to reuse an id from an upstream source
(e.g., a Kubernetes event uid).
timestamp_ns: Optional explicit event timestamp in nanoseconds since the
unix epoch. Pass 0 to let the C++ layer capture the time at
construction via absl::Now().
"""
cdef:
int _source_type
int _event_type
int _severity
str _entity_id
str _message
str _session_name
bytes _serialized_data
int _nested_event_field_number
bytes _event_id
int64_t _timestamp_ns
def __init__(
self,
int source_type,
int event_type,
int severity,
str entity_id,
str message,
str session_name,
bytes serialized_data,
int nested_event_field_number,
bytes event_id = b"",
int64_t timestamp_ns = 0,
):
self._source_type = source_type
self._event_type = event_type
self._severity = severity
self._entity_id = entity_id
self._message = message
self._session_name = session_name
self._serialized_data = serialized_data
self._nested_event_field_number = nested_event_field_number
self._event_id = event_id
self._timestamp_ns = timestamp_ns
cdef unique_ptr[CRayEventInterface] to_cpp_event(self):
"""Create the underlying C++ event. Ownership is transferred to caller."""
return CreatePythonRayEvent(
self._source_type,
self._event_type,
self._severity,
self._entity_id.encode("utf-8"),
self._message.encode("utf-8"),
self._session_name.encode("utf-8"),
self._serialized_data,
self._nested_event_field_number,
self._event_id,
self._timestamp_ns,
)
@property
def entity_id(self):
return self._entity_id
@property
def event_type(self):
return self._event_type
# module-level Singleton instance, lazily created by EventRecorder.initialize().
_event_recorder_instance = None
# Guards singleton lifecycle and emission against concurrent shutdown/initialize.
_event_recorder_lock = threading.RLock()
cdef class EventRecorder:
"""Per-process singleton for recording Ray events."""
cdef unique_ptr[CPythonEventRecorder] _recorder
def __dealloc__(self):
"""Safety-net cleanup. C++ destructor also calls Shutdown()."""
if self._recorder.get() != NULL:
self._recorder.get().Shutdown()
self._recorder.reset()
@staticmethod
def initialize(
int aggregator_port,
str node_ip,
str node_id_hex,
size_t max_buffer_size,
str metric_source = "python",
):
"""Initialize the per-process event recorder.
Creates the underlying C++ PythonEventRecorder with a background I/O
thread and gRPC client. No-op if already initialized.
Args:
aggregator_port: Port of the event aggregator server (bound on 127.0.0.1).
node_ip: IP address of the current node.
node_id_hex: Hex-encoded node ID.
max_buffer_size: Maximum number of events to buffer.
metric_source: Label for the "Source" tag on dropped-events metrics
(default "python").
"""
global _event_recorder_instance
cdef EventRecorder rec
with _event_recorder_lock:
if _event_recorder_instance is not None:
return
rec = EventRecorder()
rec._recorder.reset(
new CPythonEventRecorder(
aggregator_port,
node_ip.encode("utf-8"),
node_id_hex.encode("utf-8"),
max_buffer_size,
metric_source.encode("utf-8"),
)
)
_event_recorder_instance = rec
@staticmethod
def instance():
"""Get the per-process EventRecorder singleton.
Returns:
The EventRecorder instance if initialized, None otherwise.
"""
with _event_recorder_lock:
return _event_recorder_instance
@staticmethod
def shutdown():
"""Shutdown the event recorder.
Stops exporting events, performs a final flush, and releases all
C++ resources. After this call, emit() and emit_batch() will be
no-ops until initialize() is called again.
"""
global _event_recorder_instance
cdef EventRecorder rec
with _event_recorder_lock:
if _event_recorder_instance is None:
return
rec = <EventRecorder>_event_recorder_instance
if rec._recorder.get() != NULL:
with nogil:
rec._recorder.get().Shutdown()
rec._recorder.reset()
_event_recorder_instance = None
@staticmethod
def emit(RayEvent event):
"""Emit a single event. No-op if not initialized.
Args:
event: A RayEvent object (created via InternalEventBuilder.build()).
Returns:
True if the event was successfully queued, False otherwise.
"""
return EventRecorder.emit_batch([event])
@staticmethod
def emit_batch(list events):
"""Emit multiple events. No-op if not initialized.
Args:
events: List of RayEvent objects to emit.
Returns:
True if events were successfully queued, False otherwise.
"""
cdef EventRecorder rec
cdef c_vector[unique_ptr[CRayEventInterface]] cpp_events
cdef RayEvent ev
if not events:
return True
for ev in events:
cpp_events.push_back(move(ev.to_cpp_event()))
with _event_recorder_lock:
if _event_recorder_instance is None:
logger.debug(
"Event recorder not initialized, dropping %d events",
len(events),
)
return False
rec = <EventRecorder>_event_recorder_instance
with nogil:
rec._recorder.get().AddEvents(move(cpp_events))
return True