772 lines
23 KiB
Python
772 lines
23 KiB
Python
import asyncio
|
|
import gc
|
|
import sys
|
|
import threading
|
|
import time
|
|
from unittest.mock import Mock, patch
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
import ray
|
|
from ray._common.test_utils import wait_for_condition
|
|
from ray._raylet import ObjectRefGenerator, ObjectRefStreamEndOfStreamError
|
|
from ray.cloudpickle import dumps
|
|
from ray.exceptions import WorkerCrashedError
|
|
from ray.experimental.state.api import list_objects
|
|
|
|
|
|
class MockedWorker:
|
|
def __init__(self, mocked_core_worker):
|
|
self.core_worker = mocked_core_worker
|
|
|
|
def reset_core_worker(self):
|
|
"""Emulate the case ray.shutdown is called
|
|
and the core_worker instance is GC'ed.
|
|
"""
|
|
self.core_worker = None
|
|
|
|
def check_connected(self):
|
|
return True
|
|
|
|
|
|
@pytest.fixture
|
|
def mocked_worker():
|
|
mocked_core_worker = Mock()
|
|
mocked_core_worker.try_read_next_object_ref_stream.return_value = None
|
|
mocked_core_worker.try_read_next_object_ref_stream_n.return_value = None
|
|
mocked_core_worker.async_delete_object_ref_stream.return_value = None
|
|
mocked_core_worker.create_object_ref_stream.return_value = None
|
|
mocked_core_worker.peek_object_ref_stream.return_value = [], []
|
|
worker = MockedWorker(mocked_core_worker)
|
|
yield worker
|
|
|
|
|
|
def test_streaming_object_ref_generator_basic_unit(mocked_worker):
|
|
"""
|
|
Verify the basic case:
|
|
create a generator -> read values -> nothing more to read -> delete.
|
|
"""
|
|
with patch("ray.wait") as mocked_ray_wait:
|
|
with patch("ray.get") as mocked_ray_get:
|
|
c = mocked_worker.core_worker
|
|
generator_ref = ray.ObjectRef.from_random()
|
|
generator = ObjectRefGenerator(generator_ref, mocked_worker)
|
|
|
|
# Make sure we cannot serialize the generator.
|
|
with pytest.raises(TypeError):
|
|
dumps(generator)
|
|
|
|
# Test when there's no new ref, it returns a nil.
|
|
new_ref = ray.ObjectRef.from_random()
|
|
c.peek_object_ref_stream.return_value = (new_ref, False)
|
|
mocked_ray_wait.return_value = [], [new_ref]
|
|
ref = generator._next_sync(timeout_s=0)
|
|
assert ref.is_nil()
|
|
|
|
# When the new ref is available, next should return it.
|
|
# When peek_object_ref_stream returns is_ready = True,
|
|
# it shouldn't call ray.wait.
|
|
new_ref = ray.ObjectRef.from_random()
|
|
c.peek_object_ref_stream.return_value = (new_ref, True)
|
|
c.try_read_next_object_ref_stream.return_value = new_ref
|
|
ref = generator._next_sync(timeout_s=0)
|
|
assert new_ref == ref
|
|
|
|
# When the new ref is available, next should return it.
|
|
# When peek_object_ref_stream returns is_ready = False,
|
|
# it should wait until ray.wait returns.
|
|
for _ in range(3):
|
|
new_ref = ray.ObjectRef.from_random()
|
|
c.peek_object_ref_stream.return_value = (new_ref, False)
|
|
mocked_ray_wait.return_value = [new_ref], []
|
|
c.try_read_next_object_ref_stream.return_value = new_ref
|
|
ref = generator._next_sync(timeout_s=0)
|
|
assert new_ref == ref
|
|
|
|
# When try_read_next_object_ref_stream raises a
|
|
# ObjectRefStreamEndOfStreamError, it should raise a stop iteration.
|
|
new_ref = ray.ObjectRef.from_random()
|
|
c.peek_object_ref_stream.return_value = (new_ref, True)
|
|
c.try_read_next_object_ref_stream.side_effect = (
|
|
ObjectRefStreamEndOfStreamError("")
|
|
) # noqa
|
|
mocked_ray_get.return_value = None
|
|
|
|
with pytest.raises(StopIteration):
|
|
generator._next_sync(timeout_s=0)
|
|
|
|
|
|
def test_streaming_object_ref_generator_consume_bulk_unit(mocked_worker):
|
|
c = mocked_worker.core_worker
|
|
generator_ref = ray.ObjectRef.from_random()
|
|
generator = ObjectRefGenerator(generator_ref, mocked_worker)
|
|
|
|
generator._consume_next_ref_n(2)
|
|
|
|
c.try_read_next_object_ref_stream_n.assert_called_once_with(generator_ref, 2)
|
|
|
|
|
|
def test_streaming_object_ref_generator_task_failed_unit(mocked_worker):
|
|
"""
|
|
Verify when a task is failed by a system error,
|
|
the generator ref is returned.
|
|
"""
|
|
with patch("ray.get") as mocked_ray_get:
|
|
with patch("ray.wait") as mocked_ray_wait:
|
|
c = mocked_worker.core_worker
|
|
generator_ref = ray.ObjectRef.from_random()
|
|
generator = ObjectRefGenerator(generator_ref, mocked_worker)
|
|
|
|
# Simulate the worker failure happens.
|
|
next_ref = ray.ObjectRef.from_random()
|
|
c.peek_object_ref_stream.return_value = (next_ref, False)
|
|
mocked_ray_wait.return_value = [next_ref], []
|
|
mocked_ray_get.side_effect = WorkerCrashedError()
|
|
|
|
c.try_read_next_object_ref_stream.side_effect = (
|
|
ObjectRefStreamEndOfStreamError("")
|
|
) # noqa
|
|
ref = generator._next_sync(timeout_s=0)
|
|
# If the generator task fails by a systsem error,
|
|
# meaning the ref will raise an exception
|
|
# it should be returned.
|
|
assert ref == generator_ref
|
|
|
|
# Once exception is raised, it should always
|
|
# raise stopIteration regardless of what
|
|
# the ref contains now.
|
|
with pytest.raises(StopIteration):
|
|
ref = generator._next_sync(timeout_s=0)
|
|
|
|
|
|
def test_generator_basic(shutdown_only):
|
|
ray.init(num_cpus=1)
|
|
|
|
"""Basic cases"""
|
|
print("Test basic case")
|
|
|
|
@ray.remote
|
|
def f():
|
|
for i in range(5):
|
|
yield i
|
|
|
|
gen = f.remote()
|
|
i = 0
|
|
for ref in gen:
|
|
print(ray.get(ref))
|
|
assert i == ray.get(ref)
|
|
del ref
|
|
i += 1
|
|
|
|
"""Exceptions"""
|
|
print("Test exceptions")
|
|
|
|
@ray.remote
|
|
def f():
|
|
for i in range(5):
|
|
if i == 2:
|
|
raise ValueError
|
|
yield i
|
|
|
|
gen = f.remote()
|
|
print(ray.get(next(gen)))
|
|
print(ray.get(next(gen)))
|
|
with pytest.raises(ray.exceptions.RayTaskError) as e:
|
|
print(ray.get(next(gen)))
|
|
with pytest.raises(StopIteration):
|
|
ray.get(next(gen))
|
|
with pytest.raises(StopIteration):
|
|
ray.get(next(gen))
|
|
|
|
"""Generator Task failure"""
|
|
print("Test task failures")
|
|
|
|
@ray.remote
|
|
class A:
|
|
def getpid(self):
|
|
import os
|
|
|
|
return os.getpid()
|
|
|
|
def f(self):
|
|
for i in range(5):
|
|
time.sleep(1)
|
|
yield i
|
|
|
|
a = A.remote()
|
|
gen = a.f.remote()
|
|
i = 0
|
|
for ref in gen:
|
|
if i == 2:
|
|
ray.kill(a)
|
|
if i == 3:
|
|
with pytest.raises(ray.exceptions.RayActorError) as e:
|
|
ray.get(ref)
|
|
assert "The actor is dead because it was killed by `ray.kill`" in str(
|
|
e.value
|
|
)
|
|
break
|
|
assert i == ray.get(ref)
|
|
del ref
|
|
i += 1
|
|
for _ in range(10):
|
|
with pytest.raises(StopIteration):
|
|
next(gen)
|
|
|
|
"""Retry exceptions"""
|
|
print("Test retry exceptions")
|
|
|
|
@ray.remote
|
|
class Actor:
|
|
def __init__(self):
|
|
self.should_kill = True
|
|
|
|
def should_kill(self):
|
|
return self.should_kill
|
|
|
|
async def set(self, wait_s):
|
|
await asyncio.sleep(wait_s)
|
|
self.should_kill = False
|
|
|
|
@ray.remote(retry_exceptions=[ValueError], max_retries=10)
|
|
def f(a):
|
|
for i in range(5):
|
|
should_kill = ray.get(a.should_kill.remote())
|
|
if i == 3 and should_kill:
|
|
raise ValueError
|
|
yield i
|
|
|
|
a = Actor.remote()
|
|
gen = f.remote(a)
|
|
assert ray.get(next(gen)) == 0
|
|
assert ray.get(next(gen)) == 1
|
|
assert ray.get(next(gen)) == 2
|
|
a.set.remote(3)
|
|
assert ray.get(next(gen)) == 3
|
|
assert ray.get(next(gen)) == 4
|
|
with pytest.raises(StopIteration):
|
|
ray.get(next(gen))
|
|
|
|
"""Cancel"""
|
|
print("Test cancel")
|
|
|
|
@ray.remote
|
|
def f():
|
|
for i in range(5):
|
|
time.sleep(5)
|
|
yield i
|
|
|
|
gen = f.remote()
|
|
assert ray.get(next(gen)) == 0
|
|
ray.cancel(gen)
|
|
with pytest.raises(ray.exceptions.RayTaskError) as e:
|
|
assert ray.get(next(gen)) == 1
|
|
assert "was cancelled" in str(e.value)
|
|
with pytest.raises(StopIteration):
|
|
next(gen)
|
|
|
|
|
|
def test_streaming_generator_bad_exception_not_failing(shutdown_only, capsys):
|
|
"""This test verifies when a return value cannot be stored
|
|
e.g., because it holds a lock) if it handles failures gracefully.
|
|
|
|
Previously, when it happens, there was a check failure. This verifies
|
|
the check failure doesn't happen anymore.
|
|
"""
|
|
ray.init()
|
|
|
|
class UnserializableException(Exception):
|
|
def __init__(self):
|
|
self.lock = threading.Lock()
|
|
|
|
@ray.remote
|
|
def f():
|
|
raise UnserializableException
|
|
yield 1 # noqa
|
|
|
|
for ref in f.remote():
|
|
with pytest.raises(ray.exceptions.RayTaskError):
|
|
ray.get(ref)
|
|
captured = capsys.readouterr()
|
|
lines = captured.err.strip().split("\n")
|
|
|
|
# Verify check failure doesn't happen because we handle the error
|
|
# properly.
|
|
for line in lines:
|
|
assert "Check failed:" not in line
|
|
|
|
|
|
@pytest.mark.parametrize("crash_type", ["exception", "worker_crash"])
|
|
def test_generator_streaming_no_leak_upon_failures(
|
|
monkeypatch, shutdown_only, crash_type
|
|
):
|
|
with monkeypatch.context() as m:
|
|
m.setenv(
|
|
"RAY_testing_asio_delay_us",
|
|
"CoreWorkerService.grpc_server.ReportGeneratorItemReturns=100000:1000000",
|
|
)
|
|
ray.init(num_cpus=1)
|
|
|
|
@ray.remote
|
|
def g():
|
|
try:
|
|
gen = f.remote()
|
|
for ref in gen:
|
|
print(ref)
|
|
ray.get(ref)
|
|
except Exception:
|
|
print("exception!")
|
|
del ref
|
|
|
|
del gen
|
|
gc.collect()
|
|
|
|
# Only the ref g is alive.
|
|
def verify():
|
|
print(list_objects())
|
|
return len(list_objects()) == 1
|
|
|
|
wait_for_condition(verify)
|
|
return True
|
|
|
|
@ray.remote
|
|
def f():
|
|
for i in range(10):
|
|
time.sleep(0.2)
|
|
if i == 4:
|
|
if crash_type == "exception":
|
|
raise ValueError
|
|
else:
|
|
sys.exit(9)
|
|
yield 2
|
|
|
|
for _ in range(5):
|
|
ray.get(g.remote())
|
|
|
|
|
|
@pytest.mark.parametrize("use_actors", [False, True])
|
|
@pytest.mark.parametrize("store_in_plasma", [False, True])
|
|
def test_generator_streaming(shutdown_only, use_actors, store_in_plasma):
|
|
"""Verify the generator is working in a streaming fashion."""
|
|
ray.init()
|
|
remote_generator_fn = None
|
|
if use_actors:
|
|
|
|
@ray.remote
|
|
class Generator:
|
|
def __init__(self):
|
|
pass
|
|
|
|
def generator(self, num_returns, store_in_plasma):
|
|
for i in range(num_returns):
|
|
if store_in_plasma:
|
|
yield np.ones(1_000_000, dtype=np.int8) * i
|
|
else:
|
|
yield [i]
|
|
|
|
g = Generator.remote()
|
|
remote_generator_fn = g.generator
|
|
else:
|
|
|
|
@ray.remote(max_retries=0)
|
|
def generator(num_returns, store_in_plasma):
|
|
for i in range(num_returns):
|
|
if store_in_plasma:
|
|
yield np.ones(1_000_000, dtype=np.int8) * i
|
|
else:
|
|
yield [i]
|
|
|
|
remote_generator_fn = generator
|
|
|
|
"""Verify num_returns="streaming" is streaming"""
|
|
gen = remote_generator_fn.remote(3, store_in_plasma)
|
|
i = 0
|
|
for ref in gen:
|
|
id = ref.hex()
|
|
if store_in_plasma:
|
|
expected = np.ones(1_000_000, dtype=np.int8) * i
|
|
assert np.array_equal(ray.get(ref), expected)
|
|
else:
|
|
expected = [i]
|
|
assert ray.get(ref) == expected
|
|
|
|
del ref
|
|
|
|
wait_for_condition(
|
|
lambda id=id: len(list_objects(filters=[("object_id", "=", id)])) == 0
|
|
)
|
|
i += 1
|
|
|
|
|
|
def test_generator_dist_chain(ray_start_cluster):
|
|
"""E2E test to verify chain of generator works properly."""
|
|
cluster = ray_start_cluster
|
|
cluster.add_node(num_cpus=0, object_store_memory=1 * 1024 * 1024 * 1024)
|
|
ray.init()
|
|
cluster.add_node(num_cpus=1)
|
|
cluster.add_node(num_cpus=1)
|
|
cluster.add_node(num_cpus=1)
|
|
cluster.add_node(num_cpus=1)
|
|
|
|
@ray.remote
|
|
class ChainActor:
|
|
def __init__(self, child=None):
|
|
self.child = child
|
|
|
|
def get_data(self):
|
|
if not self.child:
|
|
for _ in range(10):
|
|
time.sleep(0.1)
|
|
yield np.ones(5 * 1024 * 1024)
|
|
else:
|
|
for data in self.child.get_data.remote():
|
|
yield ray.get(data)
|
|
|
|
chain_actor = ChainActor.remote()
|
|
chain_actor_2 = ChainActor.remote(chain_actor)
|
|
chain_actor_3 = ChainActor.remote(chain_actor_2)
|
|
chain_actor_4 = ChainActor.remote(chain_actor_3)
|
|
|
|
for ref in chain_actor_4.get_data.remote():
|
|
assert np.array_equal(np.ones(5 * 1024 * 1024), ray.get(ref))
|
|
print("getting the next data")
|
|
del ref
|
|
|
|
|
|
def test_generator_slow_pinning_requests(monkeypatch, shutdown_only):
|
|
"""
|
|
Verify when the Object pinning request from the raylet
|
|
is reported slowly, there's no refernece leak.
|
|
"""
|
|
with monkeypatch.context() as m:
|
|
m.setenv(
|
|
"RAY_testing_asio_delay_us",
|
|
"CoreWorkerService.grpc_server.PubsubLongPolling=1000000:1000000",
|
|
)
|
|
|
|
@ray.remote
|
|
def f():
|
|
yield np.ones(5 * 1024 * 1024)
|
|
|
|
for ref in f.remote():
|
|
del ref
|
|
|
|
print(list_objects())
|
|
|
|
|
|
@pytest.mark.parametrize("store_in_plasma", [False, True])
|
|
def test_actor_streaming_generator(shutdown_only, store_in_plasma):
|
|
"""Test actor/async actor with sync/async generator interfaces."""
|
|
ray.init()
|
|
|
|
@ray.remote
|
|
class Actor:
|
|
def f(self, ref):
|
|
for i in range(3):
|
|
yield i
|
|
|
|
async def async_f(self, ref):
|
|
for i in range(3):
|
|
await asyncio.sleep(0.1)
|
|
yield i
|
|
|
|
def g(self):
|
|
return 3
|
|
|
|
a = Actor.remote()
|
|
if store_in_plasma:
|
|
arr = np.random.rand(5 * 1024 * 1024)
|
|
else:
|
|
arr = 3
|
|
|
|
def verify_sync_task_executor():
|
|
generator = a.f.remote(ray.put(arr))
|
|
# Verify it works with next.
|
|
assert isinstance(generator, ObjectRefGenerator)
|
|
assert ray.get(next(generator)) == 0
|
|
assert ray.get(next(generator)) == 1
|
|
assert ray.get(next(generator)) == 2
|
|
with pytest.raises(StopIteration):
|
|
ray.get(next(generator))
|
|
|
|
# Verify it works with for.
|
|
generator = a.f.remote(ray.put(3))
|
|
for index, ref in enumerate(generator):
|
|
assert index == ray.get(ref)
|
|
|
|
def verify_async_task_executor():
|
|
# Verify it works with next.
|
|
generator = a.async_f.remote(ray.put(arr))
|
|
assert isinstance(generator, ObjectRefGenerator)
|
|
assert ray.get(next(generator)) == 0
|
|
assert ray.get(next(generator)) == 1
|
|
assert ray.get(next(generator)) == 2
|
|
|
|
# Verify it works with for.
|
|
generator = a.f.remote(ray.put(3))
|
|
for index, ref in enumerate(generator):
|
|
assert index == ray.get(ref)
|
|
|
|
async def verify_sync_task_async_generator():
|
|
# Verify anext
|
|
async_generator = a.f.remote(ray.put(arr))
|
|
assert isinstance(async_generator, ObjectRefGenerator)
|
|
for expected in range(3):
|
|
ref = await async_generator.__anext__()
|
|
assert await ref == expected
|
|
with pytest.raises(StopAsyncIteration):
|
|
await async_generator.__anext__()
|
|
|
|
# Verify async for.
|
|
async_generator = a.f.remote(ray.put(arr))
|
|
expected = 0
|
|
async for ref in async_generator:
|
|
value = await ref
|
|
assert expected == value
|
|
expected += 1
|
|
|
|
async def verify_async_task_async_generator():
|
|
async_generator = a.async_f.remote(ray.put(arr))
|
|
assert isinstance(async_generator, ObjectRefGenerator)
|
|
for expected in range(3):
|
|
ref = await async_generator.__anext__()
|
|
assert await ref == expected
|
|
with pytest.raises(StopAsyncIteration):
|
|
await async_generator.__anext__()
|
|
|
|
# Verify async for.
|
|
async_generator = a.async_f.remote(ray.put(arr))
|
|
expected = 0
|
|
async for ref in async_generator:
|
|
value = await ref
|
|
assert expected == value
|
|
expected += 1
|
|
|
|
verify_sync_task_executor()
|
|
verify_async_task_executor()
|
|
asyncio.run(verify_sync_task_async_generator())
|
|
asyncio.run(verify_async_task_async_generator())
|
|
|
|
|
|
def test_streaming_generator_num_objects_per_yield(shutdown_only):
|
|
ray.init()
|
|
|
|
@ray.remote(_num_objects_per_yield=2)
|
|
def generator():
|
|
for i in range(3):
|
|
stats = yield i, f"metadata-{i}"
|
|
assert stats is None or stats.object_creation_dur_s >= 0
|
|
|
|
gen = generator.remote()
|
|
for i in range(3):
|
|
assert ray.get(next(gen)) == i
|
|
assert ray.get(next(gen)) == f"metadata-{i}"
|
|
|
|
with pytest.raises(StopIteration):
|
|
next(gen)
|
|
|
|
@ray.remote
|
|
def per_call():
|
|
yield 1, 2
|
|
|
|
with pytest.raises(ValueError, match="_num_objects_per_yield"):
|
|
per_call.options(_num_objects_per_yield=2).remote()
|
|
|
|
|
|
def test_actor_streaming_generator_num_objects_per_yield(shutdown_only):
|
|
ray.init()
|
|
|
|
@ray.remote
|
|
class Actor:
|
|
@ray.method(_num_objects_per_yield=2)
|
|
def decorated(self):
|
|
yield "block", "metadata"
|
|
|
|
def per_call(self):
|
|
yield 1, 2
|
|
|
|
actor = Actor.remote()
|
|
|
|
gen = actor.decorated.remote()
|
|
assert ray.get(next(gen)) == "block"
|
|
assert ray.get(next(gen)) == "metadata"
|
|
with pytest.raises(StopIteration):
|
|
next(gen)
|
|
|
|
with pytest.raises(ValueError, match="_num_objects_per_yield"):
|
|
actor.per_call.options(_num_objects_per_yield=2).remote()
|
|
|
|
|
|
def test_streaming_generator_num_objects_per_yield_invalid_yield(shutdown_only):
|
|
ray.init()
|
|
|
|
@ray.remote(_num_objects_per_yield=2)
|
|
def generator():
|
|
yield (1,)
|
|
|
|
gen = generator.remote()
|
|
with pytest.raises(ValueError, match="_num_objects_per_yield=2"):
|
|
ray.get(next(gen))
|
|
|
|
with pytest.raises(StopIteration):
|
|
next(gen)
|
|
|
|
|
|
def test_streaming_generator_num_objects_per_yield_serialization_failure(shutdown_only):
|
|
ray.init()
|
|
|
|
@ray.remote(_num_objects_per_yield=2)
|
|
def generator():
|
|
yield threading.Lock(), 1
|
|
|
|
gen = generator.remote()
|
|
with pytest.raises(ray.exceptions.RayTaskError):
|
|
ray.get(next(gen))
|
|
with pytest.raises(ray.exceptions.RayTaskError):
|
|
ray.get(next(gen))
|
|
|
|
with pytest.raises(StopIteration):
|
|
next(gen)
|
|
|
|
|
|
def test_streaming_generator_num_objects_per_yield_partial_store_failure(
|
|
shutdown_only,
|
|
):
|
|
ray.init()
|
|
|
|
@ray.remote(_num_objects_per_yield=2)
|
|
def generator():
|
|
# If a later object fails after an earlier object has been stored, the
|
|
# caller should still receive a ref for every object in the grouped yield.
|
|
yield 1, threading.Lock()
|
|
|
|
gen = generator.remote()
|
|
assert ray.get(next(gen)) == 1
|
|
with pytest.raises(ray.exceptions.RayTaskError):
|
|
ray.get(next(gen))
|
|
|
|
with pytest.raises(StopIteration):
|
|
next(gen)
|
|
|
|
|
|
def test_streaming_generator_num_objects_per_yield_failure_not_retried(
|
|
shutdown_only,
|
|
):
|
|
ray.init()
|
|
|
|
@ray.remote(_num_objects_per_yield=2, retry_exceptions=True, max_retries=1)
|
|
def generator():
|
|
# Once grouped-yield IDs are allocated, the whole group must be
|
|
# reported so those temporary refs are cleared instead of retrying.
|
|
yield 1, threading.Lock()
|
|
|
|
gen = generator.remote()
|
|
assert ray.get(next(gen)) == 1
|
|
with pytest.raises(ray.exceptions.RayTaskError):
|
|
ray.get(next(gen))
|
|
|
|
with pytest.raises(StopIteration):
|
|
next(gen)
|
|
|
|
|
|
def test_streaming_generator_exception(shutdown_only):
|
|
# Verify the exceptions are correctly raised.
|
|
# Also verify the followup next will raise StopIteration.
|
|
ray.init()
|
|
|
|
@ray.remote
|
|
class Actor:
|
|
def f(self):
|
|
raise ValueError
|
|
yield 1 # noqa
|
|
|
|
async def async_f(self):
|
|
raise ValueError
|
|
yield 1 # noqa
|
|
|
|
a = Actor.remote()
|
|
g = a.f.remote()
|
|
with pytest.raises(ValueError):
|
|
ray.get(next(g))
|
|
|
|
with pytest.raises(StopIteration):
|
|
ray.get(next(g))
|
|
|
|
with pytest.raises(StopIteration):
|
|
ray.get(next(g))
|
|
|
|
g = a.async_f.remote()
|
|
with pytest.raises(ValueError):
|
|
ray.get(next(g))
|
|
|
|
with pytest.raises(StopIteration):
|
|
ray.get(next(g))
|
|
|
|
with pytest.raises(StopIteration):
|
|
ray.get(next(g))
|
|
|
|
|
|
def test_next_sync_timeout_when_generator_ref_unavailable(
|
|
monkeypatch, ray_start_cluster
|
|
):
|
|
"""_next_sync(timeout_s) must not block in its end-of-stream handling.
|
|
|
|
After all yielded refs are consumed, ``_next_sync`` calls
|
|
``ray.get(generator_ref)`` to distinguish a normal end of the stream
|
|
from a task failure. If that object is unavailable — e.g. it lived in
|
|
the plasma store of a node that died — the get must be bounded by the
|
|
caller's timeout (reporting "not ready yet" with a nil ref) instead of
|
|
blocking the caller until the object is reconstructed. A blocked caller
|
|
can deadlock: reconstruction needs a CPU, and the blocked caller may be
|
|
what releases one (see ray-project/ray#63701).
|
|
"""
|
|
# Force the generator task's return object into plasma (instead of
|
|
# being inlined with the owner) so that it is lost when its node dies.
|
|
monkeypatch.setenv("RAY_max_direct_call_object_size", "0")
|
|
cluster = ray_start_cluster
|
|
cluster.add_node(num_cpus=0)
|
|
ray.init(address=cluster.address)
|
|
worker_node = cluster.add_node(num_cpus=1)
|
|
cluster.wait_for_nodes()
|
|
|
|
@ray.remote(num_returns="streaming")
|
|
def gen():
|
|
yield 1
|
|
|
|
g = gen.remote()
|
|
# Consume the single yielded ref so that the stream is exhausted.
|
|
first = g._next_sync(timeout_s=30)
|
|
assert not first.is_nil()
|
|
|
|
# Lose the generator's return object together with its node.
|
|
cluster.remove_node(worker_node)
|
|
|
|
# End-of-stream handling must honor the timeout: a nil ref, not a block.
|
|
start = time.monotonic()
|
|
assert g._next_sync(timeout_s=0).is_nil()
|
|
assert time.monotonic() - start < 10
|
|
|
|
# The async counterpart must honor the timeout the same way.
|
|
start = time.monotonic()
|
|
assert asyncio.run(g._next_async(timeout_s=0)).is_nil()
|
|
assert time.monotonic() - start < 10
|
|
|
|
# Once a node is back, lineage reconstruction restores the return object
|
|
# and the stream terminates normally.
|
|
cluster.add_node(num_cpus=1)
|
|
cluster.wait_for_nodes()
|
|
deadline = time.monotonic() + 60
|
|
while time.monotonic() < deadline:
|
|
try:
|
|
ref = g._next_sync(timeout_s=1)
|
|
except StopIteration:
|
|
break
|
|
assert ref.is_nil()
|
|
else:
|
|
pytest.fail("Generator did not finish after the node was restored.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(pytest.main(["-sv", __file__]))
|