chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:17:40 +08:00
commit f1825c8ceb
10096 changed files with 2364182 additions and 0 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,270 @@
import asyncio
import random
import sys
import pytest
from google.protobuf.timestamp_pb2 import Timestamp
from ray.core.generated.events_base_event_pb2 import RayEvent
from ray.dashboard.modules.aggregator.multi_consumer_event_buffer import (
MultiConsumerEventBuffer,
)
def _create_test_event(
event_id: bytes = b"test",
event_type_enum=RayEvent.EventType.TASK_DEFINITION_EVENT,
message: str = "test message",
):
"""Helper function to create a test RayEvent."""
event = RayEvent()
event.event_id = event_id
event.source_type = RayEvent.SourceType.CORE_WORKER
event.event_type = event_type_enum
event.severity = RayEvent.Severity.INFO
event.message = message
event.session_name = "test_session"
# Set timestamp
timestamp = Timestamp()
timestamp.GetCurrentTime()
event.timestamp.CopyFrom(timestamp)
return event
class TestMultiConsumerEventBuffer:
@pytest.mark.asyncio
async def test_add_and_consume_event_basic(self):
"""Test basic event addition."""
buffer = MultiConsumerEventBuffer(max_size=10, max_batch_size=5)
consumer_name = "test_consumer"
await buffer.register_consumer(consumer_name)
assert await buffer.size() == 0
event = _create_test_event(b"event1")
await buffer.add_event(event)
assert await buffer.size() == 1
batch = await buffer.wait_for_batch(consumer_name, timeout_seconds=0)
assert len(batch) == 1
assert batch[0] == event
@pytest.mark.asyncio
async def test_add_event_buffer_overflow(self):
"""Test buffer overflow behavior and eviction logic."""
buffer = MultiConsumerEventBuffer(max_size=3, max_batch_size=2)
consumer_name = "test_consumer"
await buffer.register_consumer(consumer_name)
# Add events to fill buffer
events = []
event_types = [
RayEvent.EventType.TASK_DEFINITION_EVENT,
RayEvent.EventType.TASK_LIFECYCLE_EVENT,
RayEvent.EventType.ACTOR_TASK_DEFINITION_EVENT,
]
for i in range(3):
event = _create_test_event(f"event{i}".encode(), event_types[i])
events.append(event)
await buffer.add_event(event)
assert await buffer.size() == 3
# Add one more event to trigger eviction
overflow_event = _create_test_event(
b"overflow", RayEvent.EventType.TASK_PROFILE_EVENT
)
await buffer.add_event(overflow_event)
assert await buffer.size() == 3 # Still max size
@pytest.mark.asyncio
async def test_wait_for_batch_multiple_events(self):
"""Test waiting for batch when multiple events are immediately available and when when not all events are available."""
buffer = MultiConsumerEventBuffer(max_size=10, max_batch_size=3)
consumer_name = "test_consumer"
await buffer.register_consumer(consumer_name)
# Add multiple events
events = []
for i in range(5):
event = _create_test_event(f"event{i}".encode())
events.append(event)
await buffer.add_event(event)
# Should get max_batch_size events immediately
batch = await buffer.wait_for_batch(consumer_name, timeout_seconds=0.1)
assert len(batch) == 3 # max_batch_size
assert batch == events[:3]
# should now get the leftover events (< max_batch_size)
batch = await buffer.wait_for_batch(consumer_name, timeout_seconds=0.1)
assert len(batch) == 2
assert batch == events[3:]
@pytest.mark.asyncio
async def test_wait_for_batch_unknown_consumer(self):
"""Test error handling for unknown consumer."""
buffer = MultiConsumerEventBuffer(max_size=10, max_batch_size=5)
with pytest.raises(KeyError, match="unknown consumer"):
await buffer.wait_for_batch("nonexistent_consumer", timeout_seconds=0)
@pytest.mark.asyncio
async def test_register_consumer_duplicate(self):
"""Test error handling for duplicate consumer registration."""
buffer = MultiConsumerEventBuffer(max_size=10, max_batch_size=5)
consumer_name = "test_consumer"
await buffer.register_consumer(consumer_name)
with pytest.raises(
ValueError, match="consumer 'test_consumer' already registered"
):
await buffer.register_consumer(consumer_name)
@pytest.mark.asyncio
async def test_multiple_consumers_independent_cursors(self):
"""Test that multiple consumers have independent cursors."""
buffer = MultiConsumerEventBuffer(max_size=10, max_batch_size=2)
consumer_name_1 = "test_consumer_1"
consumer_name_2 = "test_consumer_2"
await buffer.register_consumer(consumer_name_1)
await buffer.register_consumer(consumer_name_2)
# Add events
events = []
for i in range(10):
event = _create_test_event(f"event{i}".encode())
events.append(event)
await buffer.add_event(event)
# Consumer 1 reads first batch
batch1 = await buffer.wait_for_batch(consumer_name_1, timeout_seconds=0.1)
assert batch1 == events[:2]
# Consumer 2 reads from beginning
batch2 = await buffer.wait_for_batch(consumer_name_2, timeout_seconds=0.1)
assert batch2 == events[:2]
# consumer 1 reads another batch
batch3 = await buffer.wait_for_batch(consumer_name_1, timeout_seconds=0.1)
assert batch3 == events[2:4]
# more events are added leading to events not consumed by consumer 2 getting evicted
# 4 events get evicted, consumer 1 has processed all 4 evicted events previously
# but consumer 2 has only processed 2 out of the 4 evicted events
for i in range(4):
event = _create_test_event(f"event{i + 10}".encode())
events.append(event)
await buffer.add_event(event)
# Just ensure buffer remains at max size
assert await buffer.size() == 10
# consumer 1 will read the next 2 events, not affected by the evictions
# consumer 1's cursor is adjusted internally to account for the evicted events
batch4 = await buffer.wait_for_batch(consumer_name_1, timeout_seconds=0.1)
assert batch4 == events[4:6]
# consumer 2 will read 2 events, skipping the evicted events
batch5 = await buffer.wait_for_batch(consumer_name_2, timeout_seconds=0.1)
assert batch5 == events[4:6] # events[2:4] are lost
@pytest.mark.asyncio
async def test_wait_for_batch_blocks_until_event_available(self):
"""Test that wait_for_batch blocks until at least one event is available."""
buffer = MultiConsumerEventBuffer(max_size=10, max_batch_size=5)
consumer_name = "test_consumer"
await buffer.register_consumer(consumer_name)
# Start waiting for batch (should block)
async def wait_for_batch():
return await buffer.wait_for_batch(consumer_name, timeout_seconds=2.0)
wait_task = asyncio.create_task(wait_for_batch())
# Wait a bit to ensure the task is waiting
await asyncio.sleep(4.0)
assert not wait_task.done()
# Add an event
event = _create_test_event(b"event1")
await buffer.add_event(event)
# Now the task should complete
batch = await wait_task
assert len(batch) == 1
assert batch[0] == event
@pytest.mark.asyncio
async def test_concurrent_producer_consumer_random_sleeps_with_overall_timeout(
self,
):
"""Producer with random sleeps and consumer reading until all events are received.
Uses an overall asyncio timeout to ensure the test fails if it hangs
before consuming all events.
"""
total_events = 40
max_batch_size = 2
buffer = MultiConsumerEventBuffer(max_size=100, max_batch_size=max_batch_size)
consumer_name = "test_consumer"
await buffer.register_consumer(consumer_name)
produced_events = []
consumed_events = []
random.seed(0)
async def producer():
for i in range(total_events):
event = _create_test_event(f"e{i}".encode())
produced_events.append(event)
await buffer.add_event(event)
await asyncio.sleep(random.uniform(0.0, 0.02))
async def consumer():
while len(consumed_events) < total_events:
batch = await buffer.wait_for_batch(consumer_name, timeout_seconds=0.1)
consumed_events.extend(batch)
# The test should fail if this times out before all events are consumed
await asyncio.wait_for(asyncio.gather(producer(), consumer()), timeout=5.0)
assert len(consumed_events) == total_events
assert consumed_events == produced_events
@pytest.mark.asyncio
async def test_events_are_evicted_once_consumed_by_all_consumers(self):
"""Test events are evicted from the buffer once they are consumed by all consumers"""
buffer = MultiConsumerEventBuffer(max_size=10, max_batch_size=2)
consumer_name_1 = "test_consumer_1"
consumer_name_2 = "test_consumer_2"
await buffer.register_consumer(consumer_name_1)
await buffer.register_consumer(consumer_name_2)
# Add events
events = []
for i in range(10):
event = _create_test_event(f"event{i}".encode())
events.append(event)
await buffer.add_event(event)
assert await buffer.size() == 10
# Consumer 1 reads first batch
batch1 = await buffer.wait_for_batch(consumer_name_1, timeout_seconds=0.1)
assert batch1 == events[:2]
# buffer size does not change as consumer 2 is yet to consume these events
assert await buffer.size() == 10
# Consumer 2 reads from beginning
batch2 = await buffer.wait_for_batch(consumer_name_2, timeout_seconds=0.1)
assert batch2 == events[:2]
# size reduces by 2 as both consumers have consumed 2 events
assert await buffer.size() == 8
if __name__ == "__main__":
sys.exit(pytest.main(["-v", __file__]))
@@ -0,0 +1,125 @@
import base64
import json
import sys
import pytest
import ray
from ray._private.test_utils import (
wait_for_condition,
wait_for_dashboard_agent_available,
)
from ray.dashboard.tests.conftest import * # noqa
_ACTOR_EVENT_PORT = 12346
@pytest.fixture(scope="session")
def httpserver_listen_address():
return ("127.0.0.1", _ACTOR_EVENT_PORT)
def test_ray_actor_events(ray_start_cluster, httpserver):
cluster = ray_start_cluster
cluster.add_node(
env_vars={
"RAY_DASHBOARD_AGGREGATOR_AGENT_EVENTS_EXPORT_ADDR": f"http://127.0.0.1:{_ACTOR_EVENT_PORT}",
"RAY_DASHBOARD_AGGREGATOR_AGENT_EXPOSABLE_EVENT_TYPES": "ACTOR_DEFINITION_EVENT,ACTOR_LIFECYCLE_EVENT",
},
_system_config={
"enable_ray_event": True,
},
)
cluster.wait_for_nodes()
head_node_id = cluster.head_node.node_id
all_nodes_ids = [node.node_id for node in cluster.list_all_nodes()]
class A:
def ping(self):
return "pong"
ray.init(address=cluster.address)
wait_for_dashboard_agent_available(cluster)
# Create an actor to trigger definition + lifecycle events
a = ray.remote(A).options(name="actor-test").remote()
ray.get(a.ping.remote())
# Check that an actor definition and a lifecycle event are published.
httpserver.expect_request("/", method="POST").respond_with_data("", status=200)
wait_for_condition(lambda: len(httpserver.log) >= 1)
req, _ = httpserver.log[0]
req_json = json.loads(req.data)
# We expect batched events containing definition then lifecycle
assert len(req_json) >= 2
# Verify event types and IDs exist
assert (
base64.b64decode(req_json[0]["actorDefinitionEvent"]["actorId"]).hex()
== a._actor_id.hex()
)
assert base64.b64decode(req_json[0]["nodeId"]).hex() == head_node_id
# Verify ActorId and state for ActorLifecycleEvents
has_alive_state = False
for actorLifeCycleEvent in req_json[1:]:
assert base64.b64decode(actorLifeCycleEvent["nodeId"]).hex() == head_node_id
assert (
base64.b64decode(
actorLifeCycleEvent["actorLifecycleEvent"]["actorId"]
).hex()
== a._actor_id.hex()
)
for stateTransition in actorLifeCycleEvent["actorLifecycleEvent"][
"stateTransitions"
]:
assert stateTransition["state"] in [
"DEPENDENCIES_UNREADY",
"PENDING_CREATION",
"ALIVE",
"RESTARTING",
"DEAD",
]
if stateTransition["state"] == "ALIVE":
has_alive_state = True
assert (
base64.b64decode(stateTransition["nodeId"]).hex() in all_nodes_ids
)
assert base64.b64decode(stateTransition["workerId"]).hex() != ""
assert has_alive_state
# Kill the actor and verify we get a DEAD state with death cause
ray.kill(a)
# Wait for the death event to be published
httpserver.expect_request("/", method="POST").respond_with_data("", status=200)
wait_for_condition(lambda: len(httpserver.log) >= 2)
has_dead_state = False
for death_req, _ in httpserver.log:
death_req_json = json.loads(death_req.data)
for actorLifeCycleEvent in death_req_json:
if "actorLifecycleEvent" in actorLifeCycleEvent:
assert (
base64.b64decode(
actorLifeCycleEvent["actorLifecycleEvent"]["actorId"]
).hex()
== a._actor_id.hex()
)
for stateTransition in actorLifeCycleEvent["actorLifecycleEvent"][
"stateTransitions"
]:
if stateTransition["state"] == "DEAD":
has_dead_state = True
assert (
stateTransition["deathCause"]["actorDiedErrorContext"][
"reason"
]
== "RAY_KILL"
)
assert has_dead_state
if __name__ == "__main__":
sys.exit(pytest.main(["-v", __file__]))
@@ -0,0 +1,164 @@
import asyncio
import sys
import uuid
import pytest
from google.protobuf.timestamp_pb2 import Timestamp
from ray._common.test_utils import async_wait_for_condition
from ray.core.generated import events_base_event_pb2
from ray.dashboard.modules.aggregator.multi_consumer_event_buffer import (
MultiConsumerEventBuffer,
)
from ray.dashboard.modules.aggregator.publisher.async_publisher_client import (
PublisherClientInterface,
PublishStats,
)
from ray.dashboard.modules.aggregator.publisher.ray_event_publisher import (
NoopPublisher,
RayEventPublisher,
)
class MockPublisherClient(PublisherClientInterface):
"""Test implementation of PublisherClientInterface."""
def __init__(
self,
batch_size: int = 1,
side_effect=lambda batch: PublishStats(True, 1, 0),
):
self.batch_size = batch_size
self.publish_calls = []
self._side_effect = side_effect
async def publish(self, batch) -> PublishStats:
self.publish_calls.append(batch)
return self._side_effect(batch)
def count_num_events_in_batch(self, batch) -> int:
return self.batch_size
async def close(self) -> None:
pass
@pytest.fixture
def base_kwargs():
"""Common kwargs for publisher initialization."""
return {
"name": "test",
"max_retries": 2,
"initial_backoff": 0,
"max_backoff": 0,
"jitter_ratio": 0,
"enable_publisher_stats": True,
}
class TestRayEventPublisher:
"""Test the main RayEventsPublisher functionality."""
@pytest.mark.asyncio
async def test_publish_with_retries_failure_then_success(self, base_kwargs):
"""Test publish that fails then succeeds."""
call_count = {"count": 0}
# fail the first publish call but succeed on retry
def side_effect(batch):
call_count["count"] += 1
if call_count["count"] == 1:
return PublishStats(False, 0, 0)
return PublishStats(True, 1, 0)
client = MockPublisherClient(side_effect=side_effect)
event_buffer = MultiConsumerEventBuffer(max_size=10, max_batch_size=10)
publisher = RayEventPublisher(
name=base_kwargs["name"] + str(uuid.uuid4()),
publish_client=client,
event_buffer=event_buffer,
max_retries=base_kwargs["max_retries"],
initial_backoff=base_kwargs["initial_backoff"],
max_backoff=base_kwargs["max_backoff"],
jitter_ratio=base_kwargs["jitter_ratio"],
)
task = asyncio.create_task(publisher.run_forever())
try:
# ensure consumer is registered
assert await publisher.wait_until_running(2.0)
# Enqueue one event into buffer
e = events_base_event_pb2.RayEvent(
event_id=b"1",
source_type=events_base_event_pb2.RayEvent.SourceType.CORE_WORKER,
event_type=events_base_event_pb2.RayEvent.EventType.TASK_DEFINITION_EVENT,
timestamp=Timestamp(seconds=123, nanos=0),
severity=events_base_event_pb2.RayEvent.Severity.INFO,
message="hello",
)
await event_buffer.add_event(e)
# wait for two publish attempts (failure then success)
await async_wait_for_condition(lambda: len(client.publish_calls) == 2)
finally:
task.cancel()
with pytest.raises(asyncio.CancelledError):
await task
@pytest.mark.asyncio
async def test_publish_with_retries_max_retries_exceeded(self, base_kwargs):
"""Test publish that fails all retries and records failed events."""
client = MockPublisherClient(
side_effect=lambda batch: PublishStats(False, 0, 0)
)
event_buffer = MultiConsumerEventBuffer(max_size=10, max_batch_size=10)
publisher = RayEventPublisher(
name=base_kwargs["name"] + str(uuid.uuid4()),
publish_client=client,
event_buffer=event_buffer,
max_retries=2, # override to finite retries
initial_backoff=0,
max_backoff=0,
jitter_ratio=0,
)
task = asyncio.create_task(publisher.run_forever())
try:
# ensure consumer is registered
assert await publisher.wait_until_running(2.0)
e = events_base_event_pb2.RayEvent(
event_id=b"1",
source_type=events_base_event_pb2.RayEvent.SourceType.CORE_WORKER,
event_type=events_base_event_pb2.RayEvent.EventType.TASK_DEFINITION_EVENT,
timestamp=Timestamp(seconds=123, nanos=0),
severity=events_base_event_pb2.RayEvent.Severity.INFO,
message="hello",
)
await event_buffer.add_event(e)
# wait for publish attempts (initial + 2 retries)
await async_wait_for_condition(lambda: len(client.publish_calls) == 3)
assert len(client.publish_calls) == 3
finally:
task.cancel()
with pytest.raises(asyncio.CancelledError):
await task
class TestNoopPublisher:
"""Test no-op publisher implementation."""
@pytest.mark.asyncio
async def test_all_methods_noop(self):
"""Test that run_forever can be cancelled and metrics return expected values."""
publisher = NoopPublisher()
# Start and cancel run_forever
task = asyncio.create_task(publisher.run_forever())
task.cancel()
with pytest.raises(asyncio.CancelledError):
await task
if __name__ == "__main__":
sys.exit(pytest.main(["-v", __file__]))
@@ -0,0 +1,58 @@
import base64
import json
import sys
import pytest
import ray
from ray._private.test_utils import (
wait_for_condition,
wait_for_dashboard_agent_available,
)
from ray.dashboard.tests.conftest import * # noqa
_RAY_EVENT_PORT = 12345
@pytest.fixture(scope="session")
def httpserver_listen_address():
return ("127.0.0.1", _RAY_EVENT_PORT)
def test_ray_job_events(ray_start_cluster, httpserver):
cluster = ray_start_cluster
cluster.add_node(
env_vars={
"RAY_DASHBOARD_AGGREGATOR_AGENT_EVENTS_EXPORT_ADDR": f"http://127.0.0.1:{_RAY_EVENT_PORT}",
"RAY_DASHBOARD_AGGREGATOR_AGENT_EXPOSABLE_EVENT_TYPES": "DRIVER_JOB_DEFINITION_EVENT,DRIVER_JOB_LIFECYCLE_EVENT",
},
_system_config={
"enable_ray_event": True,
},
)
cluster.wait_for_nodes()
ray.init(address=cluster.address)
wait_for_dashboard_agent_available(cluster)
# Submit a ray job
@ray.remote
def f():
return 1
ray.get(f.remote())
# Check that a driver job event with the correct job id is published.
httpserver.expect_request("/", method="POST").respond_with_data("", status=200)
wait_for_condition(lambda: len(httpserver.log) >= 1)
req, _ = httpserver.log[0]
req_json = json.loads(req.data)
head_node_id = cluster.head_node.node_id
assert base64.b64decode(req_json[0]["nodeId"]).hex() == head_node_id
assert (
base64.b64decode(req_json[0]["driverJobDefinitionEvent"]["jobId"]).hex()
== ray.get_runtime_context().get_job_id()
)
if __name__ == "__main__":
sys.exit(pytest.main(["-v", __file__]))
@@ -0,0 +1,75 @@
import base64
import json
import os
import socket
import sys
import pytest
import ray
from ray._private.test_utils import (
wait_for_condition,
wait_for_dashboard_agent_available,
)
from ray.dashboard.tests.conftest import * # noqa
_RAY_EVENT_PORT = 12345
@pytest.fixture(scope="session")
def httpserver_listen_address():
return ("127.0.0.1", _RAY_EVENT_PORT)
def test_ray_node_events(ray_start_cluster, httpserver):
cluster = ray_start_cluster
cluster.add_node(
node_name="test-head-node",
env_vars={
"RAY_DASHBOARD_AGGREGATOR_AGENT_EVENTS_EXPORT_ADDR": f"http://127.0.0.1:{_RAY_EVENT_PORT}",
"RAY_DASHBOARD_AGGREGATOR_AGENT_EXPOSABLE_EVENT_TYPES": "NODE_DEFINITION_EVENT,NODE_LIFECYCLE_EVENT",
},
_system_config={
"enable_ray_event": True,
},
)
cluster.wait_for_nodes()
head_node_id = cluster.head_node.node_id
ray.init(address=cluster.address)
wait_for_dashboard_agent_available(cluster)
# Check that a node definition and a node lifecycle event are published.
httpserver.expect_request("/", method="POST").respond_with_data("", status=200)
wait_for_condition(lambda: len(httpserver.log) >= 1)
req, _ = httpserver.log[0]
req_json = json.loads(req.data)
assert len(req_json) == 2
assert base64.b64decode(req_json[0]["nodeId"]).hex() == head_node_id
assert (
base64.b64decode(req_json[0]["nodeDefinitionEvent"]["nodeId"]).hex()
== cluster.head_node.node_id
)
node_def_event = req_json[0]["nodeDefinitionEvent"]
assert node_def_event["hostname"] == socket.gethostname()
assert node_def_event["nodeName"] == "test-head-node"
# instanceId and instanceTypeName are set via env vars by cloud providers.
# In local/CI environments these are typically empty.
assert node_def_event["instanceId"] == os.environ.get("RAY_CLOUD_INSTANCE_ID", "")
assert node_def_event["instanceTypeName"] == os.environ.get(
"RAY_CLOUD_INSTANCE_TYPE_NAME", ""
)
assert base64.b64decode(req_json[1]["nodeId"]).hex() == head_node_id
assert (
base64.b64decode(req_json[1]["nodeLifecycleEvent"]["nodeId"]).hex()
== cluster.head_node.node_id
)
assert req_json[1]["nodeLifecycleEvent"]["stateTransitions"][0]["state"] == "ALIVE"
assert (
req_json[1]["nodeLifecycleEvent"]["stateTransitions"][0]["aliveSubState"]
== "UNSPECIFIED"
)
if __name__ == "__main__":
sys.exit(pytest.main(["-v", __file__]))
@@ -0,0 +1,117 @@
import base64
import json
import sys
import time
import pytest
import ray
from ray._private.test_utils import (
wait_for_condition,
wait_for_dashboard_agent_available,
)
from ray.dashboard.tests.conftest import * # noqa
_PLATFORM_EVENT_PORT = 12348
@pytest.fixture(scope="session")
def httpserver_listen_address():
return ("127.0.0.1", _PLATFORM_EVENT_PORT)
def test_ray_platform_events(ray_start_cluster, httpserver):
cluster = ray_start_cluster
cluster.add_node(
env_vars={
"RAY_DASHBOARD_AGGREGATOR_AGENT_EVENTS_EXPORT_ADDR": f"http://127.0.0.1:{_PLATFORM_EVENT_PORT}",
"RAY_DASHBOARD_AGGREGATOR_AGENT_EXPOSABLE_EVENT_TYPES": "PLATFORM_EVENT",
"RAY_ENABLE_PYTHON_RAY_EVENT_TYPES": "PLATFORM_EVENT",
},
_system_config={
"enable_ray_event": True,
},
)
cluster.wait_for_nodes()
head_node_id = cluster.head_node.node_id
ray.init(address=cluster.address)
wait_for_dashboard_agent_available(cluster)
# Define a task that explicitly initializes and emits a platform event via EventRecorder
@ray.remote
def emit_test_platform_event(aggregator_port, node_ip, node_id):
from ray._common.observability.platform_events import PlatformEventBuilder
from ray._raylet import EventRecorder
from ray.core.generated.events_base_event_pb2 import RayEvent
from ray.core.generated.platform_event_pb2 import Source
EventRecorder.initialize(
aggregator_port=aggregator_port,
node_ip=node_ip,
node_id_hex=node_id,
max_buffer_size=1000,
metric_source="platform_events",
)
builder = PlatformEventBuilder(
event_uid="uid-test-platform-e2e",
platform=Source.Platform.KUBERNETES,
object_kind="Pod",
object_name="test-pod-name",
reason="OOMKilled",
message="Container exited with code 137",
severity=RayEvent.Severity.WARNING,
component="kubelet",
)
cython_event = builder.build(
event_id=b"uid-test-platform-e2e",
timestamp_ns=int(time.time() * 1e9),
)
EventRecorder.emit(cython_event)
EventRecorder.shutdown()
return True
# Expect the POST request on the HTTP server
httpserver.expect_request("/", method="POST").respond_with_data("", status=200)
# Fetch the aggregator agent's address from GCS
from ray._private.test_utils import GcsClient, get_dashboard_agent_address
gcs_client = GcsClient(address=cluster.address)
agent_address = get_dashboard_agent_address(gcs_client, head_node_id)
ip, port_str = agent_address.split(":")
aggregator_port = int(port_str)
# Execute the remote task to emit the event on the node
ray.get(emit_test_platform_event.remote(aggregator_port, ip, head_node_id))
# Wait for the HTTP log collector to receive the batched payload
wait_for_condition(lambda: len(httpserver.log) >= 1, timeout=20)
# Validate the captured POST payload
req, _ = httpserver.log[0]
req_json = json.loads(req.data)
assert len(req_json) >= 1
platform_event_entry = None
for entry in req_json:
if "platformEvent" in entry:
platform_event_entry = entry
break
assert platform_event_entry is not None
assert platform_event_entry["eventType"] == "PLATFORM_EVENT"
assert base64.b64decode(platform_event_entry["nodeId"]).hex() == head_node_id
pe_data = platform_event_entry["platformEvent"]
assert pe_data["objectKind"] == "Pod"
assert pe_data["objectName"] == "test-pod-name"
assert pe_data["reason"] == "OOMKilled"
assert pe_data["message"] == "Container exited with code 137"
assert pe_data["source"]["platform"] == "KUBERNETES"
assert pe_data["source"]["component"] == "kubelet"
if __name__ == "__main__":
sys.exit(pytest.main(["-v", __file__]))
@@ -0,0 +1,115 @@
import sys
import pytest
from ray.core.generated.events_event_aggregator_service_pb2 import TaskEventsMetadata
from ray.dashboard.modules.aggregator.task_events_metadata_buffer import (
TaskEventsMetadataBuffer,
)
def _create_test_metadata(dropped_task_ids: list = None, attempt_number=1):
"""Helper function to create test metadata"""
metadata = TaskEventsMetadata()
if dropped_task_ids:
for task_id in dropped_task_ids:
attempt = metadata.dropped_task_attempts.add()
attempt.task_id = task_id.encode()
attempt.attempt_number = attempt_number
return metadata
def _result_to_attempts_list(result):
"""Normalize return value from buffer.get() to a python list of attempts."""
if hasattr(result, "dropped_task_attempts"):
attempts = result.dropped_task_attempts
else:
attempts = result
return list(attempts)
def _drain_all_attempts(buffer: TaskEventsMetadataBuffer):
"""Drain the buffer completely via public API and return list of bytes task_ids.
Continues calling get() until it returns an empty set of attempts.
"""
collected_ids = []
num_metadata_entries = 0
while True:
result = buffer.get()
attempts = _result_to_attempts_list(result)
if len(attempts) == 0:
break
num_metadata_entries += 1
collected_ids.extend([a.task_id for a in attempts])
return collected_ids, num_metadata_entries
class TestTaskMetadataBuffer:
"""tests for TaskMetadataBuffer class"""
def test_merge_and_get(self):
"""Test merging multiple metadata objects and verify task attempts are combined."""
buffer = TaskEventsMetadataBuffer(
max_buffer_size=100, max_dropped_attempts_per_metadata_entry=10
)
# Create two separate metadata objects with different task IDs
metadata1 = _create_test_metadata(["task_1", "task_2"])
metadata2 = _create_test_metadata(["task_3", "task_4"])
# Merge both metadata objects
buffer.merge(metadata1)
buffer.merge(metadata2)
# Get the merged results
result = buffer.get()
attempts = _result_to_attempts_list(result)
# Verify we have all 4 task attempts
assert len(attempts) == 4
# Verify all expected task IDs are present
task_ids = [attempt.task_id for attempt in attempts]
assert sorted(task_ids) == [b"task_1", b"task_2", b"task_3", b"task_4"]
@pytest.mark.parametrize(
"max_attempts_per_metadata_entry,num_tasks,max_buffer_size,expected_drop_attempts,expected_num_metadata_entries",
[
# No overflow, two metadata entries should be created
(2, 3, 100, 0, 2),
# No overflow, three metadata entries should be created
(5, 15, 100, 0, 3),
# Overflow scenario: buffer too small, ensure drop count is tracked.
(1, 4, 2, 2, 2),
],
)
def test_buffer_merge_and_overflow(
self,
max_attempts_per_metadata_entry,
num_tasks,
max_buffer_size,
expected_drop_attempts,
expected_num_metadata_entries,
):
buffer = TaskEventsMetadataBuffer(
max_buffer_size=max_buffer_size,
max_dropped_attempts_per_metadata_entry=max_attempts_per_metadata_entry,
)
for i in range(num_tasks):
test_metadata = _create_test_metadata([f"task_{i}"])
buffer.merge(test_metadata)
# Drain everything and verify number of attempts in buffer is as expected
drained_ids, num_metadata_entries = _drain_all_attempts(buffer)
assert len(drained_ids) == num_tasks - expected_drop_attempts
assert num_metadata_entries == expected_num_metadata_entries
# Buffer should now be empty
assert len(_result_to_attempts_list(buffer.get())) == 0
if __name__ == "__main__":
sys.exit(pytest.main(["-v", __file__]))