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

1655 lines
58 KiB
Python

import json
import os
import pathlib
import sys
import threading
import time
from dataclasses import asdict
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
from unittest.mock import Mock, patch
import pytest
import requests
from jsonschema import validate
import ray
import ray._common.usage.usage_constants as usage_constants
import ray._common.usage.usage_lib as ray_usage_lib
from ray._common.test_utils import (
run_string_as_driver,
wait_for_condition,
)
from ray._common.usage.usage_lib import ClusterConfigToReport, UsageStatsEnabledness
from ray._private.accelerators import NvidiaGPUAcceleratorManager
from ray._private.test_utils import (
format_web_url,
wait_until_server_available,
)
from ray._raylet import GcsClient
from ray.autoscaler._private.cli_logger import cli_logger
from ray.tests.conftest import * # noqa: F403
from ray.util.placement_group import (
placement_group,
)
schema = {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"schema_version": {"type": "string"},
"source": {"type": "string"},
"session_id": {"type": "string"},
"ray_version": {"type": "string"},
"git_commit": {"type": "string"},
"os": {"type": "string"},
"python_version": {"type": "string"},
"collect_timestamp_ms": {"type": "integer"},
"session_start_timestamp_ms": {"type": "integer"},
"cloud_provider": {"type": ["null", "string"]},
"min_workers": {"type": ["null", "integer"]},
"max_workers": {"type": ["null", "integer"]},
"head_node_instance_type": {"type": ["null", "string"]},
"libc_version": {"type": ["null", "string"]},
"worker_node_instance_types": {
"type": ["null", "array"],
"items": {"type": "string"},
},
"total_num_cpus": {"type": ["null", "integer"]},
"total_num_gpus": {"type": ["null", "integer"]},
"total_memory_gb": {"type": ["null", "number"]},
"total_object_store_memory_gb": {"type": ["null", "number"]},
"library_usages": {
"type": ["null", "array"],
"items": {"type": "string"},
},
"hardware_usages": {
"type": ["null", "array"],
"items": {"type": "string"},
},
"total_success": {"type": "integer"},
"total_failed": {"type": "integer"},
"seq_number": {"type": "integer"},
"extra_usage_tags": {"type": ["null", "object"]},
"total_num_nodes": {"type": ["null", "integer"]},
"total_num_running_jobs": {"type": ["null", "integer"]},
},
"additionalProperties": False,
}
def file_exists(temp_dir: Path):
for path in temp_dir.iterdir():
if usage_constants.USAGE_STATS_FILE in str(path):
return True
return False
def read_file(temp_dir: Path, column: str):
usage_stats_file = temp_dir / usage_constants.USAGE_STATS_FILE
with usage_stats_file.open() as f:
result = json.load(f)
return result[column]
def print_dashboard_log():
session_dir = ray._private.worker.global_worker.node.address_info["session_dir"]
session_path = Path(session_dir)
log_dir_path = session_path / "logs"
paths = list(log_dir_path.iterdir())
contents = None
for path in paths:
if "dashboard.log" in str(path):
with open(str(path), "r") as f:
contents = f.readlines()
from pprint import pprint
pprint(contents)
@pytest.fixture
def gcs_storage_type():
storage = "redis" if os.environ.get("RAY_REDIS_ADDRESS") else "memory"
yield storage
@pytest.fixture
def reset_usage_stats():
yield
ray.experimental.internal_kv._internal_kv_reset()
ray_usage_lib._recorded_library_usages.clear()
ray_usage_lib._recorded_extra_usage_tags.clear()
@pytest.fixture
def reset_ray_version_commit():
saved_ray_version = ray.__version__
saved_ray_commit = ray.__commit__
yield
ray.__version__ = saved_ray_version
ray.__commit__ = saved_ray_commit
@pytest.fixture
def start_usage_stats_server():
class UsageStatsServer(BaseHTTPRequestHandler):
num_reports = 0
report_payload = None
def do_POST(self):
content_length = int(self.headers["Content-Length"])
post_data = self.rfile.read(content_length)
UsageStatsServer.num_reports += 1
UsageStatsServer.report_payload = json.loads(post_data)
self.send_response(200)
self.send_header("Content-type", "text/html")
self.end_headers()
server = HTTPServer(("127.0.0.1", 8000), UsageStatsServer)
server_thread = threading.Thread(target=server.serve_forever)
server_thread.start()
yield UsageStatsServer
server.shutdown()
server_thread.join()
@pytest.mark.parametrize("ray_client", [True, False])
def test_get_extra_usage_tags_to_report(
monkeypatch, call_ray_start, reset_usage_stats, ray_client, gcs_storage_type
):
if os.environ.get("RAY_MINIMAL") == "1" and ray_client:
pytest.skip("Skipping due to we don't have ray client in minimal.")
with monkeypatch.context() as m:
# Test a normal case.
m.setenv("RAY_USAGE_STATS_EXTRA_TAGS", "key=val;key2=val2")
result = ray_usage_lib.get_extra_usage_tags_to_report(
ray.experimental.internal_kv.internal_kv_get_gcs_client()
)
assert result["key"] == "val"
assert result["key2"] == "val2"
m.setenv("RAY_USAGE_STATS_EXTRA_TAGS", "key=val;key2=val2;")
result = ray_usage_lib.get_extra_usage_tags_to_report(
ray.experimental.internal_kv.internal_kv_get_gcs_client()
)
assert result["key"] == "val"
assert result["key2"] == "val2"
# Test that the env var is not given.
m.delenv("RAY_USAGE_STATS_EXTRA_TAGS")
result = ray_usage_lib.get_extra_usage_tags_to_report(
ray.experimental.internal_kv.internal_kv_get_gcs_client()
)
assert result == {}
# Test the parsing failure.
m.setenv("RAY_USAGE_STATS_EXTRA_TAGS", "key=val,key2=val2")
result = ray_usage_lib.get_extra_usage_tags_to_report(
ray.experimental.internal_kv.internal_kv_get_gcs_client()
)
assert result == {}
# Test differnt types of parsing failures.
m.setenv("RAY_USAGE_STATS_EXTRA_TAGS", "key=v=al,key2=val2")
result = ray_usage_lib.get_extra_usage_tags_to_report(
ray.experimental.internal_kv.internal_kv_get_gcs_client()
)
assert result == {}
address = call_ray_start
ray.init(address=address)
m.setenv("RAY_USAGE_STATS_EXTRA_TAGS", "key=val")
driver = """
import ray
import ray._common.usage.usage_lib as ray_usage_lib
ray_usage_lib.record_extra_usage_tag(ray_usage_lib.TagKey._TEST1, "val1")
ray.init(address="{}")
ray_usage_lib.record_extra_usage_tag(ray_usage_lib.TagKey._TEST2, "val2")
""".format(
"ray://127.0.0.1:10001" if ray_client else address
)
run_string_as_driver(driver)
wait_for_condition(
lambda: ray_usage_lib.get_extra_usage_tags_to_report(
ray.experimental.internal_kv.internal_kv_get_gcs_client()
)
== {
"key": "val",
"_test1": "val1",
"_test2": "val2",
"actor_num_created": "0",
"pg_num_created": "0",
"num_actor_creation_tasks": "0",
"num_actor_tasks": "0",
"num_normal_tasks": "0",
"num_drivers": "2",
"gcs_storage": gcs_storage_type,
"dashboard_used": "False",
},
timeout=10,
)
# Make sure the value is overwritten.
ray_usage_lib.record_extra_usage_tag(ray_usage_lib.TagKey._TEST2, "val3")
wait_for_condition(
lambda: ray_usage_lib.get_extra_usage_tags_to_report(
ray.experimental.internal_kv.internal_kv_get_gcs_client()
)
== {
"key": "val",
"_test1": "val1",
"_test2": "val3",
"actor_num_created": "0",
"pg_num_created": "0",
"num_actor_creation_tasks": "0",
"num_actor_tasks": "0",
"num_normal_tasks": "0",
"num_drivers": "2",
"gcs_storage": gcs_storage_type,
"dashboard_used": "False",
},
timeout=10,
)
@pytest.mark.skipif(
sys.platform != "linux" and sys.platform != "linux2",
reason="memory monitor only on linux currently",
)
def test_worker_crash_increment_stats():
@ray.remote
def crasher():
exit(1)
@ray.remote
def oomer():
mem = []
while True:
mem.append([0] * 1000000000)
with ray.init() as ctx:
with pytest.raises(ray.exceptions.WorkerCrashedError):
ray.get(crasher.options(max_retries=1).remote())
with pytest.raises(ray.exceptions.OutOfMemoryError):
ray.get(oomer.options(max_retries=0).remote())
gcs_client = ray._raylet.GcsClient(address=ctx.address_info["gcs_address"])
wait_for_condition(
lambda: "worker_crash_system_error"
in ray_usage_lib.get_extra_usage_tags_to_report(gcs_client),
timeout=4,
)
result = ray_usage_lib.get_extra_usage_tags_to_report(gcs_client)
assert "worker_crash_system_error" in result
assert result["worker_crash_system_error"] == "2"
assert "worker_crash_oom" in result
assert result["worker_crash_oom"] == "1"
def test_actor_stats(reset_usage_stats):
@ray.remote
class Actor:
def foo(self):
pass
with ray.init(
_system_config={"metrics_report_interval_ms": 1000},
) as ctx:
gcs_client = ray._raylet.GcsClient(address=ctx.address_info["gcs_address"])
actor = Actor.remote()
wait_for_condition(
lambda: ray_usage_lib.get_extra_usage_tags_to_report(gcs_client).get(
"actor_num_created"
)
== "1"
and ray_usage_lib.get_extra_usage_tags_to_report(gcs_client).get(
"num_actor_creation_tasks"
)
== "1",
timeout=10,
)
actor = Actor.remote()
wait_for_condition(
lambda: ray_usage_lib.get_extra_usage_tags_to_report(gcs_client).get(
"actor_num_created"
)
== "2"
and ray_usage_lib.get_extra_usage_tags_to_report(gcs_client).get(
"num_actor_creation_tasks"
)
== "2"
and ray_usage_lib.get_extra_usage_tags_to_report(gcs_client).get(
"num_actor_tasks"
)
== "0",
timeout=10,
)
ray.get(actor.foo.remote())
wait_for_condition(
lambda: ray_usage_lib.get_extra_usage_tags_to_report(gcs_client).get(
"actor_num_created"
)
== "2"
and ray_usage_lib.get_extra_usage_tags_to_report(gcs_client).get(
"num_actor_creation_tasks"
)
== "2"
and ray_usage_lib.get_extra_usage_tags_to_report(gcs_client).get(
"num_actor_tasks"
)
== "1",
timeout=10,
)
del actor
def test_pg_stats(reset_usage_stats):
with ray.init(
num_cpus=3,
_system_config={"metrics_report_interval_ms": 1000},
) as ctx:
gcs_client = ray._raylet.GcsClient(address=ctx.address_info["gcs_address"])
pg = placement_group([{"CPU": 1}], strategy="STRICT_PACK")
ray.get(pg.ready())
wait_for_condition(
lambda: ray_usage_lib.get_extra_usage_tags_to_report(gcs_client).get(
"pg_num_created"
)
== "1",
timeout=5,
)
pg1 = placement_group([{"CPU": 1}], strategy="STRICT_PACK")
ray.get(pg1.ready())
wait_for_condition(
lambda: ray_usage_lib.get_extra_usage_tags_to_report(gcs_client).get(
"pg_num_created"
)
== "2",
timeout=5,
)
def test_task_stats(reset_usage_stats):
@ray.remote
def foo():
pass
with ray.init(
_system_config={"metrics_report_interval_ms": 1000},
) as ctx:
gcs_client = ray._raylet.GcsClient(address=ctx.address_info["gcs_address"])
wait_for_condition(
lambda: ray_usage_lib.get_extra_usage_tags_to_report(gcs_client).get(
"num_normal_tasks"
)
== "0"
and ray_usage_lib.get_extra_usage_tags_to_report(gcs_client).get(
"num_drivers"
)
== "1",
timeout=10,
)
ray.get(foo.remote())
wait_for_condition(
lambda: ray_usage_lib.get_extra_usage_tags_to_report(gcs_client).get(
"num_normal_tasks"
)
== "1",
timeout=10,
)
ray.get(foo.remote())
wait_for_condition(
lambda: ray_usage_lib.get_extra_usage_tags_to_report(gcs_client).get(
"num_normal_tasks"
)
== "2"
and ray_usage_lib.get_extra_usage_tags_to_report(gcs_client).get(
"num_drivers"
)
== "1",
timeout=10,
)
def test_usage_stats_enabledness(monkeypatch, tmp_path, reset_usage_stats):
with monkeypatch.context() as m:
m.setenv("RAY_USAGE_STATS_ENABLED", "1")
assert (
ray_usage_lib._usage_stats_enabledness()
is UsageStatsEnabledness.ENABLED_EXPLICITLY
)
with monkeypatch.context() as m:
m.setenv("RAY_USAGE_STATS_ENABLED", "0")
assert (
ray_usage_lib._usage_stats_enabledness()
is UsageStatsEnabledness.DISABLED_EXPLICITLY
)
with monkeypatch.context() as m:
m.setenv("RAY_USAGE_STATS_ENABLED", "xxx")
with pytest.raises(ValueError):
ray_usage_lib._usage_stats_enabledness()
with monkeypatch.context() as m:
m.delenv("RAY_USAGE_STATS_ENABLED", raising=False)
tmp_usage_stats_config_path = tmp_path / "config.json"
monkeypatch.setenv(
"RAY_USAGE_STATS_CONFIG_PATH", str(tmp_usage_stats_config_path)
)
tmp_usage_stats_config_path.write_text('{"usage_stats": true}')
assert (
ray_usage_lib._usage_stats_enabledness()
is UsageStatsEnabledness.ENABLED_EXPLICITLY
)
tmp_usage_stats_config_path.write_text('{"usage_stats": false}')
assert (
ray_usage_lib._usage_stats_enabledness()
is UsageStatsEnabledness.DISABLED_EXPLICITLY
)
tmp_usage_stats_config_path.write_text('{"usage_stats": "xxx"}')
with pytest.raises(ValueError):
ray_usage_lib._usage_stats_enabledness()
tmp_usage_stats_config_path.write_text("")
assert (
ray_usage_lib._usage_stats_enabledness()
is UsageStatsEnabledness.ENABLED_BY_DEFAULT
)
tmp_usage_stats_config_path.unlink()
assert (
ray_usage_lib._usage_stats_enabledness()
is UsageStatsEnabledness.ENABLED_BY_DEFAULT
)
def test_set_usage_stats_enabled_via_config(monkeypatch, tmp_path, reset_usage_stats):
tmp_usage_stats_config_path = tmp_path / "config1.json"
monkeypatch.setenv("RAY_USAGE_STATS_CONFIG_PATH", str(tmp_usage_stats_config_path))
ray_usage_lib.set_usage_stats_enabled_via_config(True)
assert '{"usage_stats": true}' == tmp_usage_stats_config_path.read_text()
ray_usage_lib.set_usage_stats_enabled_via_config(False)
assert '{"usage_stats": false}' == tmp_usage_stats_config_path.read_text()
tmp_usage_stats_config_path.write_text('"xxx"')
ray_usage_lib.set_usage_stats_enabled_via_config(True)
assert '{"usage_stats": true}' == tmp_usage_stats_config_path.read_text()
tmp_usage_stats_config_path.unlink()
os.makedirs(os.path.dirname(tmp_usage_stats_config_path / "xxx.txt"), exist_ok=True)
with pytest.raises(Exception, match="Failed to enable usage stats.*"):
ray_usage_lib.set_usage_stats_enabled_via_config(True)
@pytest.fixture
def clear_loggers():
"""Remove handlers from all loggers"""
yield
import logging
loggers = [logging.getLogger()] + list(logging.Logger.manager.loggerDict.values())
for logger in loggers:
handlers = getattr(logger, "handlers", [])
for handler in handlers:
logger.removeHandler(handler)
# NOTE: We are clearing loggers because otherwise, the next test's
# logger will access the capsys buffer that's already closed when this
# test is terminated. It seems like loggers are shared across drivers
# although we call ray.shutdown().
def test_usage_stats_prompt(
monkeypatch,
capsys,
tmp_path,
reset_usage_stats,
shutdown_only,
clear_loggers,
reset_ray_version_commit,
):
"""
Test usage stats prompt is shown in the proper cases.
"""
with monkeypatch.context() as m:
m.setenv("RAY_USAGE_STATS_ENABLED", "1")
m.setenv("RAY_USAGE_STATS_PROMPT_ENABLED", "0")
ray_usage_lib.show_usage_stats_prompt(cli=True)
captured = capsys.readouterr()
assert usage_constants.USAGE_STATS_ENABLED_FOR_CLI_MESSAGE not in captured.out
assert usage_constants.USAGE_STATS_ENABLED_FOR_CLI_MESSAGE not in captured.err
with monkeypatch.context() as m:
m.setenv("RAY_USAGE_STATS_ENABLED", "1")
m.setenv("RAY_USAGE_STATS_PROMPT_ENABLED", "0")
ray_usage_lib.show_usage_stats_prompt(cli=False)
captured = capsys.readouterr()
assert (
usage_constants.USAGE_STATS_ENABLED_FOR_RAY_INIT_MESSAGE not in captured.out
)
assert (
usage_constants.USAGE_STATS_ENABLED_FOR_RAY_INIT_MESSAGE not in captured.err
)
with monkeypatch.context() as m:
m.setenv("RAY_USAGE_STATS_ENABLED", "0")
ray_usage_lib.show_usage_stats_prompt(cli=True)
captured = capsys.readouterr()
assert usage_constants.USAGE_STATS_DISABLED_MESSAGE in captured.out
with monkeypatch.context() as m:
m.delenv("RAY_USAGE_STATS_ENABLED", raising=False)
tmp_usage_stats_config_path = tmp_path / "config1.json"
m.setenv("RAY_USAGE_STATS_CONFIG_PATH", str(tmp_usage_stats_config_path))
# Usage stats collection is enabled by default.
ray_usage_lib.show_usage_stats_prompt(cli=True)
captured = capsys.readouterr()
assert (
usage_constants.USAGE_STATS_ENABLED_BY_DEFAULT_FOR_CLI_MESSAGE
in captured.out
)
with monkeypatch.context() as m:
# Win impl relies on kbhit() instead of select()
# so the pipe trick won't work.
if sys.platform != "win32":
m.delenv("RAY_USAGE_STATS_ENABLED", raising=False)
saved_interactive = cli_logger.interactive
saved_stdin = sys.stdin
tmp_usage_stats_config_path = tmp_path / "config2.json"
m.setenv("RAY_USAGE_STATS_CONFIG_PATH", str(tmp_usage_stats_config_path))
cli_logger.interactive = True
(r_pipe, w_pipe) = os.pipe()
sys.stdin = open(r_pipe)
os.write(w_pipe, b"y\n")
ray_usage_lib.show_usage_stats_prompt(cli=True)
captured = capsys.readouterr()
assert usage_constants.USAGE_STATS_CONFIRMATION_MESSAGE in captured.out
assert usage_constants.USAGE_STATS_ENABLED_FOR_CLI_MESSAGE in captured.out
cli_logger.interactive = saved_interactive
sys.stdin = saved_stdin
with monkeypatch.context() as m:
if sys.platform != "win32":
m.delenv("RAY_USAGE_STATS_ENABLED", raising=False)
saved_interactive = cli_logger.interactive
saved_stdin = sys.stdin
tmp_usage_stats_config_path = tmp_path / "config3.json"
m.setenv("RAY_USAGE_STATS_CONFIG_PATH", str(tmp_usage_stats_config_path))
cli_logger.interactive = True
(r_pipe, w_pipe) = os.pipe()
sys.stdin = open(r_pipe)
os.write(w_pipe, b"n\n")
ray_usage_lib.show_usage_stats_prompt(cli=True)
captured = capsys.readouterr()
assert usage_constants.USAGE_STATS_CONFIRMATION_MESSAGE in captured.out
assert usage_constants.USAGE_STATS_DISABLED_MESSAGE in captured.out
cli_logger.interactive = saved_interactive
sys.stdin = saved_stdin
with monkeypatch.context() as m:
m.delenv("RAY_USAGE_STATS_ENABLED", raising=False)
saved_interactive = cli_logger.interactive
saved_stdin = sys.stdin
tmp_usage_stats_config_path = tmp_path / "config4.json"
m.setenv("RAY_USAGE_STATS_CONFIG_PATH", str(tmp_usage_stats_config_path))
cli_logger.interactive = True
(r_pipe, w_pipe) = os.pipe()
sys.stdin = open(r_pipe)
ray_usage_lib.show_usage_stats_prompt(cli=True)
captured = capsys.readouterr()
assert usage_constants.USAGE_STATS_CONFIRMATION_MESSAGE in captured.out
assert usage_constants.USAGE_STATS_ENABLED_FOR_CLI_MESSAGE in captured.out
cli_logger.interactive = saved_interactive
sys.stdin = saved_stdin
with monkeypatch.context() as m:
# Usage stats is not enabled for ray.init() unless it's nightly wheel.
m.delenv("RAY_USAGE_STATS_ENABLED", raising=False)
tmp_usage_stats_config_path = tmp_path / "config5.json"
m.setenv("RAY_USAGE_STATS_CONFIG_PATH", str(tmp_usage_stats_config_path))
ray.__version__ = "2.0.0"
ray.__commit__ = "xyzf"
ray.init()
ray.shutdown()
captured = capsys.readouterr()
assert (
usage_constants.USAGE_STATS_ENABLED_BY_DEFAULT_FOR_RAY_INIT_MESSAGE
not in captured.out
)
assert (
usage_constants.USAGE_STATS_ENABLED_FOR_RAY_INIT_MESSAGE not in captured.out
)
with monkeypatch.context() as m:
# Usage stats is enabled for ray.init() for nightly wheel.
m.delenv("RAY_USAGE_STATS_ENABLED", raising=False)
tmp_usage_stats_config_path = tmp_path / "config6.json"
m.setenv("RAY_USAGE_STATS_CONFIG_PATH", str(tmp_usage_stats_config_path))
ray.__version__ = "2.0.0.dev0"
ray.__commit__ = "xyzf"
ray.init()
ray.shutdown()
captured = capsys.readouterr()
assert (
usage_constants.USAGE_STATS_ENABLED_BY_DEFAULT_FOR_RAY_INIT_MESSAGE
in captured.out
)
with monkeypatch.context() as m:
m.setenv("RAY_USAGE_STATS_ENABLED", "0")
ray.__version__ = "2.0.0.dev0"
ray.__commit__ = "xyzf"
ray.init()
ray.shutdown()
captured = capsys.readouterr()
assert usage_constants.USAGE_STATS_DISABLED_MESSAGE in captured.out
with monkeypatch.context() as m:
m.setenv("RAY_USAGE_STATS_ENABLED", "1")
ray.__version__ = "2.0.0.dev0"
ray.__commit__ = "xyzf"
ray.init()
ray.shutdown()
captured = capsys.readouterr()
assert usage_constants.USAGE_STATS_ENABLED_FOR_RAY_INIT_MESSAGE in captured.out
def test_is_nightly_wheel(reset_ray_version_commit):
ray.__version__ = "2.0.0"
ray.__commit__ = "xyz"
assert not ray_usage_lib.is_nightly_wheel()
ray.__version__ = "2.0.0dev0"
ray.__commit__ = "{{RAY_COMMIT_SHA}}"
assert not ray_usage_lib.is_nightly_wheel()
ray.__version__ = "2.0.0dev0"
ray.__commit__ = "xyz"
assert ray_usage_lib.is_nightly_wheel()
def test_usage_lib_cluster_metadata_generation(
monkeypatch, ray_start_cluster, reset_usage_stats
):
with monkeypatch.context() as m:
m.setenv("RAY_USAGE_STATS_ENABLED", "1")
m.setenv("RAY_USAGE_STATS_REPORT_URL", "http://127.0.0.1:8000")
cluster = ray_start_cluster
cluster.add_node(num_cpus=0)
ray.init(address=cluster.address)
"""
Test metadata stored is equivalent to `_generate_cluster_metadata`.
"""
meta = ray_usage_lib._generate_cluster_metadata(ray_init_cluster=False)
cluster_metadata = ray_usage_lib.get_cluster_metadata(
ray.experimental.internal_kv.internal_kv_get_gcs_client()
)
# Remove fields that are dynamically changed.
assert meta.pop("session_start_timestamp_ms")
assert cluster_metadata.pop("session_start_timestamp_ms")
assert meta == cluster_metadata
"""
Make sure put & get works properly.
"""
cluster_metadata = ray_usage_lib.put_cluster_metadata(
ray.experimental.internal_kv.internal_kv_get_gcs_client(),
ray_init_cluster=False,
)
assert cluster_metadata == ray_usage_lib.get_cluster_metadata(
ray.experimental.internal_kv.internal_kv_get_gcs_client()
)
@pytest.mark.skipif(
os.environ.get("RAY_MINIMAL") == "1",
reason="This test is not supposed to work for minimal installation.",
)
def test_usage_stats_enabled_endpoint(
monkeypatch, ray_start_cluster, reset_usage_stats
):
import requests
with monkeypatch.context() as m:
m.setenv("RAY_USAGE_STATS_ENABLED", "0")
m.setenv("RAY_USAGE_STATS_PROMPT_ENABLED", "0")
cluster = ray_start_cluster
cluster.add_node(num_cpus=0)
context = ray.init(address=cluster.address)
webui_url = context["webui_url"]
assert wait_until_server_available(webui_url)
webui_url = format_web_url(webui_url)
response = requests.get(f"{webui_url}/usage_stats_enabled")
assert response.status_code == 200
assert response.json()["result"] is True
assert response.json()["data"]["usageStatsEnabled"] is False
assert response.json()["data"]["usageStatsPromptEnabled"] is False
@pytest.mark.skipif(
os.environ.get("RAY_MINIMAL") == "1",
reason="This test is not supposed to work for minimal installation.",
)
def test_get_cluster_id(ray_start_cluster, reset_usage_stats):
import requests
cluster = ray_start_cluster
cluster.add_node(num_cpus=0)
context = ray.init(address=cluster.address)
webui_url = context["webui_url"]
assert wait_until_server_available(webui_url)
webui_url = format_web_url(webui_url)
response = requests.get(f"{webui_url}/cluster_id")
assert response.status_code == 200
assert response.json()["result"] is True
gcs_client = GcsClient(address=ray.get_runtime_context().gcs_address)
assert response.json()["data"]["clusterId"] == gcs_client.cluster_id.hex()
def test_hardware_usages(shutdown_only, reset_usage_stats):
with patch.object(
NvidiaGPUAcceleratorManager,
"get_current_node_accelerator_type",
return_value="TestAccelerator",
), patch.object(
ray._private.utils, "get_current_node_cpu_model_name", return_value="TestCPU"
):
ray.init(num_gpus=4)
assert set(
ray_usage_lib.get_hardware_usages_to_report(
ray.experimental.internal_kv.internal_kv_get_gcs_client()
)
) == {"TestAccelerator", "TestCPU"}
@pytest.mark.skipif(
os.environ.get("RAY_MINIMAL") == "1",
reason="This test is not supposed to work for minimal installation "
"since we import libraries.",
)
@pytest.mark.parametrize("ray_client", [True, False])
def test_library_usages(call_ray_start, reset_usage_stats, ray_client):
from ray.job_submission import JobSubmissionClient
address = call_ray_start
ray.init(address=address)
driver = """
import ray
import ray._common.usage.usage_lib as ray_usage_lib
ray_usage_lib.record_library_usage("pre_init")
ray.init(address="{}")
ray_usage_lib.record_library_usage("post_init")
class Actor:
def get_actor_metadata(self):
return "metadata"
from ray.util.actor_group import ActorGroup
actor_group = ActorGroup(Actor)
actor_pool = ray.util.actor_pool.ActorPool([])
from ray.util.multiprocessing import Pool
pool = Pool()
from ray.util.queue import Queue
queue = Queue()
import joblib
from ray.util.joblib import register_ray
register_ray()
with joblib.parallel_backend("ray"):
pass
""".format(
"ray://127.0.0.1:10001" if ray_client else address
)
run_string_as_driver(driver)
if sys.platform != "win32":
job_submission_client = JobSubmissionClient("http://127.0.0.1:8265")
job_id = job_submission_client.submit_job(entrypoint="ls")
wait_for_condition(
lambda: job_submission_client.get_job_status(job_id)
== ray.job_submission.JobStatus.SUCCEEDED
)
library_usages = ray_usage_lib.get_library_usages_to_report(
ray.experimental.internal_kv.internal_kv_get_gcs_client()
)
expected = {
"pre_init",
"post_init",
"util.ActorGroup",
"util.ActorPool",
"util.multiprocessing.Pool",
"util.Queue",
"util.joblib",
"core",
}
if sys.platform != "win32":
expected.add("job_submission")
if ray_client:
expected.add("client")
assert set(library_usages) == expected
def test_usage_lib_cluster_metadata_generation_usage_disabled(
monkeypatch, shutdown_only, reset_usage_stats
):
"""
Make sure only version information is generated when usage stats are not enabled.
"""
with monkeypatch.context() as m:
m.setenv("RAY_USAGE_STATS_ENABLED", "0")
meta = ray_usage_lib._generate_cluster_metadata(ray_init_cluster=False)
assert "ray_version" in meta
assert "python_version" in meta
assert "ray_init_cluster" in meta
assert len(meta) == 3
def test_usage_lib_get_total_num_running_jobs_to_report(
ray_start_cluster, reset_usage_stats
):
cluster = ray_start_cluster
cluster.add_node(num_cpus=1)
gcs_client = ray._raylet.GcsClient(address=cluster.gcs_address)
assert ray_usage_lib.get_total_num_running_jobs_to_report(gcs_client) == 0
ray.init(address=cluster.address)
assert ray_usage_lib.get_total_num_running_jobs_to_report(gcs_client) == 1
ray.shutdown()
ray.init(address=cluster.address)
# Make sure the previously finished job is not counted.
assert ray_usage_lib.get_total_num_running_jobs_to_report(gcs_client) == 1
ray.shutdown()
def test_usage_lib_get_total_num_alive_nodes_to_report(
ray_start_cluster, reset_usage_stats
):
cluster = ray_start_cluster
cluster.add_node(num_cpus=1)
ray.init(address=cluster.address)
worker_node = cluster.add_node(num_cpus=2)
assert (
ray_usage_lib.get_total_num_alive_nodes_to_report(
ray.experimental.internal_kv.internal_kv_get_gcs_client()
)
== 2
)
cluster.remove_node(worker_node)
# Make sure only alive nodes are counted
assert (
ray_usage_lib.get_total_num_alive_nodes_to_report(
ray.experimental.internal_kv.internal_kv_get_gcs_client()
)
== 1
)
@pytest.mark.parametrize("enable_v2", [True, False])
def test_usage_lib_get_cluster_status_to_report(
enable_v2, shutdown_only, reset_usage_stats
):
ray.init(
num_cpus=3,
num_gpus=1,
object_store_memory=2**30,
_system_config={"enable_autoscaler_v2": enable_v2},
)
# Wait for monitor.py to update cluster status
wait_for_condition(
lambda: ray_usage_lib.get_cluster_status_to_report(
ray.experimental.internal_kv.internal_kv_get_gcs_client()
).total_num_cpus
== 3,
timeout=10,
)
cluster_status_to_report = ray_usage_lib.get_cluster_status_to_report(
ray.experimental.internal_kv.internal_kv_get_gcs_client()
)
assert cluster_status_to_report.total_num_cpus == 3
assert cluster_status_to_report.total_num_gpus == 1
assert cluster_status_to_report.total_memory_gb > 0
assert cluster_status_to_report.total_object_store_memory_gb == 1.0
def test_usage_lib_get_cluster_config_to_report(
monkeypatch, tmp_path, reset_usage_stats
):
cluster_config_file_path = tmp_path / "ray_bootstrap_config.yaml"
""" Test minimal cluster config"""
cluster_config_file_path.write_text(
"""
cluster_name: minimal
max_workers: 1
provider:
type: aws
region: us-west-2
availability_zone: us-west-2a
"""
)
cluster_config_to_report = ray_usage_lib.get_cluster_config_to_report(
cluster_config_file_path
)
assert cluster_config_to_report.cloud_provider == "aws"
assert cluster_config_to_report.min_workers is None
assert cluster_config_to_report.max_workers == 1
assert cluster_config_to_report.head_node_instance_type is None
assert cluster_config_to_report.worker_node_instance_types is None
cluster_config_file_path.write_text(
"""
cluster_name: full
min_workers: 1
provider:
type: gcp
head_node_type: head_node
available_node_types:
head_node:
node_config:
InstanceType: m5.large
min_workers: 0
max_workers: 0
aws_worker_node:
node_config:
InstanceType: m3.large
min_workers: 0
max_workers: 0
azure_worker_node:
node_config:
azure_arm_parameters:
vmSize: Standard_D2s_v3
gcp_worker_node:
node_config:
machineType: n1-standard-2
"""
)
cluster_config_to_report = ray_usage_lib.get_cluster_config_to_report(
cluster_config_file_path
)
assert cluster_config_to_report.cloud_provider == "gcp"
assert cluster_config_to_report.min_workers == 1
assert cluster_config_to_report.max_workers is None
assert cluster_config_to_report.head_node_instance_type == "m5.large"
assert set(cluster_config_to_report.worker_node_instance_types) == {
"m3.large",
"Standard_D2s_v3",
"n1-standard-2",
}
cluster_config_file_path.write_text(
"""
cluster_name: full
head_node_type: head_node
available_node_types:
worker_node_1:
node_config:
ImageId: xyz
worker_node_2:
resources: {}
worker_node_3:
node_config:
InstanceType: m5.large
"""
)
cluster_config_to_report = ray_usage_lib.get_cluster_config_to_report(
cluster_config_file_path
)
assert cluster_config_to_report.cloud_provider is None
assert cluster_config_to_report.min_workers is None
assert cluster_config_to_report.max_workers is None
assert cluster_config_to_report.head_node_instance_type is None
assert cluster_config_to_report.worker_node_instance_types == ["m5.large"]
cluster_config_file_path.write_text("[invalid")
cluster_config_to_report = ray_usage_lib.get_cluster_config_to_report(
cluster_config_file_path
)
assert cluster_config_to_report == ClusterConfigToReport()
cluster_config_to_report = ray_usage_lib.get_cluster_config_to_report(
tmp_path / "does_not_exist.yaml"
)
# can't assert cloud_provider here because it will be set based on
# where the test is actually running
assert cluster_config_to_report.head_node_instance_type is None
assert cluster_config_to_report.min_workers is None
assert cluster_config_to_report.max_workers is None
assert cluster_config_to_report.worker_node_instance_types is None
monkeypatch.setenv("KUBERNETES_SERVICE_HOST", "localhost")
cluster_config_to_report = ray_usage_lib.get_cluster_config_to_report(
tmp_path / "does_not_exist.yaml"
)
# starts with because additional cloud provider info may be added depending on
# the environment
assert cluster_config_to_report.cloud_provider.startswith("kubernetes")
assert cluster_config_to_report.min_workers is None
assert cluster_config_to_report.max_workers is None
assert cluster_config_to_report.head_node_instance_type is None
assert cluster_config_to_report.worker_node_instance_types is None
monkeypatch.setenv("RAY_USAGE_STATS_KUBERAY_IN_USE", "1")
cluster_config_to_report = ray_usage_lib.get_cluster_config_to_report(
tmp_path / "does_not_exist.yaml"
)
assert cluster_config_to_report.cloud_provider.startswith("kuberay")
def test_usage_lib_report_data(
monkeypatch,
ray_start_cluster,
tmp_path,
start_usage_stats_server,
reset_usage_stats,
):
with monkeypatch.context() as m:
m.setenv("RAY_USAGE_STATS_ENABLED", "1")
m.setenv("RAY_USAGE_STATS_REPORT_URL", "http://127.0.0.1:8000")
cluster = ray_start_cluster
cluster.add_node(num_cpus=0)
ray.init(address=cluster.address)
"""
Make sure the generated data is following the schema.
"""
cluster_config_file_path = tmp_path / "ray_bootstrap_config.yaml"
cluster_config_file_path.write_text(
"""
cluster_name: minimal
max_workers: 1
provider:
type: aws
region: us-west-2
availability_zone: us-west-2a
"""
)
cluster_config_to_report = ray_usage_lib.get_cluster_config_to_report(
cluster_config_file_path
)
d = ray_usage_lib.generate_report_data(
cluster_config_to_report,
2,
2,
2,
ray.worker.global_worker.gcs_client.address,
ray.worker.global_worker.gcs_client.cluster_id.hex(),
)
validate(instance=asdict(d), schema=schema)
"""
Make sure writing to a file works as expected
"""
client = ray_usage_lib.UsageReportClient()
temp_dir = Path(tmp_path)
client.write_usage_data(d, temp_dir)
wait_for_condition(lambda: file_exists(temp_dir))
"""
Make sure report usage data works as expected
"""
usage_stats_server = start_usage_stats_server
# Query our endpoint over HTTP.
wait_for_condition(
lambda: client.report_usage_data("http://127.0.0.1:8000", d), timeout=30
)
assert usage_stats_server.report_payload == asdict(d)
def test_usage_report_e2e(
monkeypatch,
ray_start_cluster,
tmp_path,
start_usage_stats_server,
reset_usage_stats,
gcs_storage_type,
):
"""
Test usage report works e2e with env vars.
"""
cluster_config_file_path = tmp_path / "ray_bootstrap_config.yaml"
cluster_config_file_path.write_text(
"""
cluster_name: minimal
max_workers: 1
provider:
type: aws
region: us-west-2
availability_zone: us-west-2a
"""
)
with patch.object(
ray._private.utils, "get_current_node_cpu_model_name", return_value="TestCPU"
), monkeypatch.context() as m:
m.setenv("HOME", str(tmp_path))
m.setenv("RAY_USAGE_STATS_ENABLED", "1")
m.setenv("RAY_USAGE_STATS_REPORT_URL", "http://127.0.0.1:8000")
m.setenv("RAY_USAGE_STATS_REPORT_INTERVAL_S", "1")
m.setenv("RAY_USAGE_STATS_EXTRA_TAGS", "extra_k1=extra_v1")
usage_stats_server = start_usage_stats_server
cluster = ray_start_cluster
node = cluster.add_node(num_cpus=3)
ray_usage_lib.record_extra_usage_tag(ray_usage_lib.TagKey._TEST1, "extra_v2")
ray.init(address=cluster.address)
@ray.remote
def f():
pass
ray.get(f.remote())
ray_usage_lib.record_extra_usage_tag(ray_usage_lib.TagKey._TEST2, "extra_v3")
"""
Verify the usage stats are reported to the server.
"""
print("Verifying usage stats report.")
# Since the interval is 1 second, there must have been
# more than 5 requests sent within 30 seconds.
try:
wait_for_condition(lambda: usage_stats_server.num_reports > 5, timeout=30)
except Exception:
print_dashboard_log()
raise
payload = usage_stats_server.report_payload
ray_version, python_version = ray._private.utils.compute_version_info()
assert payload["ray_version"] == ray_version
assert payload["python_version"] == python_version
assert payload["schema_version"] == "0.1"
assert payload["os"] == sys.platform
if sys.platform != "linux":
payload["libc_version"] is None
else:
import platform
assert (
payload["libc_version"]
== f"{platform.libc_ver()[0]}:{platform.libc_ver()[1]}"
)
assert payload["source"] == "OSS"
assert payload["session_id"] == node.cluster_id.hex()
assert payload["cloud_provider"] == "aws"
assert payload["min_workers"] is None
assert payload["max_workers"] == 1
assert payload["head_node_instance_type"] is None
assert payload["worker_node_instance_types"] is None
assert payload["total_num_cpus"] == 3
assert payload["total_num_gpus"] is None
assert payload["total_memory_gb"] > 0
assert payload["total_object_store_memory_gb"] > 0
assert int(payload["extra_usage_tags"]["actor_num_created"]) >= 0
assert int(payload["extra_usage_tags"]["pg_num_created"]) >= 0
assert int(payload["extra_usage_tags"]["num_actor_creation_tasks"]) >= 0
assert int(payload["extra_usage_tags"]["num_actor_tasks"]) >= 0
assert int(payload["extra_usage_tags"]["num_normal_tasks"]) >= 0
assert int(payload["extra_usage_tags"]["num_drivers"]) >= 0
payload["extra_usage_tags"]["actor_num_created"] = "0"
payload["extra_usage_tags"]["pg_num_created"] = "0"
payload["extra_usage_tags"]["num_actor_creation_tasks"] = "0"
payload["extra_usage_tags"]["num_actor_tasks"] = "0"
payload["extra_usage_tags"]["num_normal_tasks"] = "0"
payload["extra_usage_tags"]["num_drivers"] = "0"
expected_payload = {
"extra_k1": "extra_v1",
"_test1": "extra_v2",
"_test2": "extra_v3",
"dashboard_metrics_grafana_enabled": "False",
"dashboard_metrics_prometheus_enabled": "False",
"actor_num_created": "0",
"pg_num_created": "0",
"num_actor_creation_tasks": "0",
"num_actor_tasks": "0",
"num_normal_tasks": "0",
"num_drivers": "0",
"gcs_storage": gcs_storage_type,
"dashboard_used": "False",
}
assert payload["extra_usage_tags"] == expected_payload
assert payload["total_num_nodes"] == 1
assert payload["total_num_running_jobs"] == 1
assert set(payload["library_usages"]) == {"core"}
assert payload["hardware_usages"] == ["TestCPU"]
validate(instance=payload, schema=schema)
"""
Verify the usage_stats.json is updated.
"""
print("Verifying usage stats write.")
global_node = ray._private.worker._global_node
temp_dir = pathlib.Path(global_node.get_session_dir_path())
wait_for_condition(lambda: file_exists(temp_dir), timeout=30)
timestamp_old = read_file(temp_dir, "usage_stats")["collect_timestamp_ms"]
success_old = read_file(temp_dir, "usage_stats")["total_success"]
# Test if the timestampe has been updated.
wait_for_condition(
lambda: timestamp_old
< read_file(temp_dir, "usage_stats")["collect_timestamp_ms"]
)
wait_for_condition(
lambda: success_old < read_file(temp_dir, "usage_stats")["total_success"]
)
assert read_file(temp_dir, "success")
def test_first_usage_report_delayed(monkeypatch, ray_start_cluster, reset_usage_stats):
with monkeypatch.context() as m:
m.setenv("RAY_USAGE_STATS_ENABLED", "1")
m.setenv("RAY_USAGE_STATS_REPORT_URL", "http://127.0.0.1:8000")
m.setenv("RAY_USAGE_STATS_REPORT_INTERVAL_S", "10")
cluster = ray_start_cluster
cluster.add_node(num_cpus=0)
ray.init(address=cluster.address)
# The first report should be delayed for 10s.
time.sleep(5)
session_dir = ray._private.worker.global_worker.node.address_info["session_dir"]
session_path = Path(session_dir)
assert not (session_path / usage_constants.USAGE_STATS_FILE).exists()
time.sleep(10)
assert (session_path / usage_constants.USAGE_STATS_FILE).exists()
def test_usage_report_disabled_ray_init_cluster(
monkeypatch, start_usage_stats_server, reset_usage_stats, shutdown_only
):
"""
Make sure we don't send anything to the server for the ray.init cluster
if usage stats is disabled.
"""
with monkeypatch.context() as m:
m.setenv("RAY_USAGE_STATS_ENABLED", "0")
m.setenv("RAY_USAGE_STATS_REPORT_URL", "http://127.0.0.1:8000")
m.setenv("RAY_USAGE_STATS_REPORT_INTERVAL_S", "1")
usage_stats_server = start_usage_stats_server
ray.init()
time.sleep(5)
assert usage_stats_server.num_reports == 0
"""
Verify the correct logs are printed.
"""
session_dir = ray._private.worker.global_worker.node.address_info["session_dir"]
session_path = Path(session_dir)
log_dir_path = session_path / "logs"
paths = list(log_dir_path.iterdir())
contents = None
for path in paths:
if "dashboard.log" in str(path):
with open(str(path), "r") as f:
contents = f.readlines()
break
assert contents is not None
assert any(["Usage reporting is disabled" in c for c in contents])
def test_usage_report_disabled(
monkeypatch, ray_start_cluster, start_usage_stats_server, reset_usage_stats
):
"""
Make sure usage report module is disabled when the env var is not set.
It also verifies that the failure message is not printed (note that
the invalid report url is given as an env var).
"""
with monkeypatch.context() as m:
m.setenv("RAY_USAGE_STATS_ENABLED", "0")
m.setenv("RAY_USAGE_STATS_REPORT_URL", "http://127.0.0.1:8000")
m.setenv("RAY_USAGE_STATS_REPORT_INTERVAL_S", "1")
m.delenv("RAY_USAGE_STATS_RAY_INIT_CLUSTER", raising=False)
usage_stats_server = start_usage_stats_server
cluster = ray_start_cluster
cluster.add_node(num_cpus=0)
ray.init(address=cluster.address)
"""
Verify the disabled usage stat is reported to the server.
"""
wait_for_condition(lambda: usage_stats_server.num_reports == 1)
# We should have one and only one report to the server.
time.sleep(5)
assert usage_stats_server.num_reports == 1
payload = usage_stats_server.report_payload
assert payload["schema_version"] == "0.1"
assert payload["source"] == "OSS"
assert payload["collect_timestamp_ms"] > 0
assert len({k: v for k, v in payload.items() if v is not None}) == 3
"""
Verify the correct logs are printed.
"""
session_dir = ray._private.worker.global_worker.node.address_info["session_dir"]
session_path = Path(session_dir)
log_dir_path = session_path / "logs"
paths = list(log_dir_path.iterdir())
contents = None
for path in paths:
if "dashboard.log" in str(path):
with open(str(path), "r") as f:
contents = f.readlines()
break
assert contents is not None
assert any(["Usage reporting is disabled" in c for c in contents])
assert all(["Usage report request failed" not in c for c in contents])
def test_usage_file_error_message(monkeypatch, ray_start_cluster, reset_usage_stats):
"""
Make sure the usage report file is generated with a proper
error message when the report is failed.
"""
with monkeypatch.context() as m:
m.setenv("RAY_USAGE_STATS_ENABLED", "1")
m.setenv("RAY_USAGE_STATS_REPORT_URL", "http://127.0.0.1:8000")
m.setenv("RAY_USAGE_STATS_REPORT_INTERVAL_S", "1")
cluster = ray_start_cluster
cluster.add_node(num_cpus=0)
ray.init(address=cluster.address)
global_node = ray._private.worker._global_node
temp_dir = pathlib.Path(global_node.get_session_dir_path())
try:
wait_for_condition(lambda: file_exists(temp_dir), timeout=30)
except Exception:
print_dashboard_log()
raise
error_message = read_file(temp_dir, "error")
failure_old = read_file(temp_dir, "usage_stats")["total_failed"]
report_success = read_file(temp_dir, "success")
# Test if the timestampe has been updated.
assert (
"HTTPConnectionPool(host='127.0.0.1', port=8000): "
"Max retries exceeded with url:"
) in error_message
assert not report_success
try:
wait_for_condition(
lambda: failure_old < read_file(temp_dir, "usage_stats")["total_failed"]
)
except Exception:
print_dashboard_log()
read_file(temp_dir, "usage_stats")["total_failed"]
raise
assert read_file(temp_dir, "usage_stats")["total_success"] == 0
def test_usage_stats_tags(
monkeypatch, ray_start_cluster, reset_usage_stats, gcs_storage_type
):
"""
Test usage tags are correctly reported.
"""
with monkeypatch.context() as m:
m.setenv("RAY_USAGE_STATS_ENABLED", "1")
m.setenv("RAY_USAGE_STATS_REPORT_URL", "http://127.0.0.1:8000/usage")
m.setenv("RAY_USAGE_STATS_REPORT_INTERVAL_S", "1")
m.setenv("RAY_USAGE_STATS_EXTRA_TAGS", "key=val;key2=val2")
cluster = ray_start_cluster
cluster.add_node(num_cpus=3)
cluster.add_node(num_cpus=3)
context = ray.init(address=cluster.address)
"""
Verify the usage_stats.json contains the lib usage.
"""
temp_dir = pathlib.Path(context.address_info["session_dir"])
wait_for_condition(lambda: file_exists(temp_dir), timeout=30)
def verify():
tags = read_file(temp_dir, "usage_stats")["extra_usage_tags"]
num_nodes = read_file(temp_dir, "usage_stats")["total_num_nodes"]
assert tags == {
"key": "val",
"key2": "val2",
"dashboard_metrics_grafana_enabled": "False",
"dashboard_metrics_prometheus_enabled": "False",
"gcs_storage": gcs_storage_type,
"dashboard_used": "False",
"actor_num_created": "0",
"pg_num_created": "0",
"num_actor_creation_tasks": "0",
"num_actor_tasks": "0",
"num_normal_tasks": "0",
"num_drivers": "1",
}
assert num_nodes == 2
return True
wait_for_condition(verify)
def test_usage_stats_gcs_query_failure(
monkeypatch, ray_start_cluster, reset_usage_stats
):
"""Test None data is reported when the GCS query is failed."""
with monkeypatch.context() as m:
m.setenv(
"RAY_testing_asio_delay_us",
"NodeInfoGcsService.grpc_server.GetAllNodeInfo=2000000:2000000",
)
cluster = ray_start_cluster
cluster.add_node(num_cpus=3)
ray.init(address=cluster.address)
assert (
ray_usage_lib.get_total_num_alive_nodes_to_report(
ray.experimental.internal_kv.internal_kv_get_gcs_client(), timeout=1
)
is None
)
def test_usages_stats_available_when_dashboard_not_included(
monkeypatch, ray_start_cluster, reset_usage_stats
):
"""
Test library usage is correctly reported when they are imported from
workers.
"""
with monkeypatch.context() as m:
m.setenv("RAY_USAGE_STATS_ENABLED", "1")
m.setenv("RAY_USAGE_STATS_REPORT_URL", "http://127.0.0.1:8000/usage")
m.setenv("RAY_USAGE_STATS_REPORT_INTERVAL_S", "1")
cluster = ray_start_cluster
cluster.add_node(num_cpus=1, include_dashboard=False)
ray.init(address=cluster.address)
"""
Verify the usage_stats.json contains the lib usage.
"""
temp_dir = pathlib.Path(cluster.head_node.get_session_dir_path())
wait_for_condition(lambda: file_exists(temp_dir), timeout=30)
def verify():
return read_file(temp_dir, "usage_stats")["seq_number"] > 2
wait_for_condition(verify)
def test_usages_stats_dashboard(monkeypatch, ray_start_cluster, reset_usage_stats):
"""
Test dashboard usage metrics are correctly reported.
This is tested on both minimal / non minimal ray.
"""
with monkeypatch.context() as m:
m.setenv("RAY_USAGE_STATS_ENABLED", "1")
m.setenv("RAY_USAGE_STATS_REPORT_URL", "http://127.0.0.1:8000/usage")
m.setenv("RAY_USAGE_STATS_REPORT_INTERVAL_S", "1")
cluster = ray_start_cluster
cluster.add_node(num_cpus=0)
addr = ray.init(address=cluster.address)
"""
Verify the usage_stats.json contains the lib usage.
"""
temp_dir = pathlib.Path(ray._private.worker._global_node.get_session_dir_path())
webui_url = format_web_url(addr["webui_url"])
wait_for_condition(lambda: file_exists(temp_dir), timeout=30)
def verify_dashboard_not_used():
dashboard_used = read_file(temp_dir, "usage_stats")["extra_usage_tags"][
"dashboard_used"
]
return dashboard_used == "False"
wait_for_condition(verify_dashboard_not_used)
if os.environ.get("RAY_MINIMAL") == "1":
# In the minimal Ray, dashboard is not available.
return
# Open the dashboard will set the dashboard_used == "True".
resp = requests.get(webui_url)
resp.raise_for_status()
def verify_dashboard_used():
dashboard_used = read_file(temp_dir, "usage_stats")["extra_usage_tags"][
"dashboard_used"
]
if os.environ.get("RAY_MINIMAL") == "1":
return dashboard_used == "False"
else:
return dashboard_used == "True"
wait_for_condition(verify_dashboard_used)
def test_get_cloud_from_metadata_requests(monkeypatch):
def create_mock_response(url: str, provider: str, error_providers: list[str]):
# Create a mock response based on the URL.
mock_response = Mock()
if url == "http://metadata.google.internal/computeMetadata/v1":
# GCP endpoint
if "gcp" in error_providers:
print("raising")
raise requests.exceptions.ConnectionError()
mock_response.status_code = 200 if provider == "gcp" else 400
elif url == "http://169.254.169.254/latest/meta-data/":
# AWS endpoint
if "aws" in error_providers:
raise requests.exceptions.ConnectionError()
# Azure IMDS returns 400 (not 404) when queried with AWS endpoint format
# because Azure requires the "Metadata: true" header (not sent in AWS queries).
# See: https://learn.microsoft.com/en-us/azure/virtual-machines/instance-metadata-service#errors-and-debugging
if provider == "azure":
mock_response.status_code = (
400 # Bad Request (missing headers/wrong path)
)
else:
mock_response.status_code = 200 if provider == "aws" else 404
elif url == "http://169.254.169.254/metadata/instance?api-version=2021-12-13":
# Azure endpoint
if "azure" in error_providers:
raise requests.exceptions.ConnectionError()
# AWS IMDS returns 404 when queried with Azure endpoint format
# because Azure's URL path doesn't exist on AWS metadata service.
# See: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html#instance-metadata-returns
if provider == "aws":
mock_response.status_code = 404 # Not Found
else:
mock_response.status_code = 200 if provider == "azure" else 400
return mock_response
with patch("requests.get") as mock_get:
mock_get.side_effect = lambda url, **kwargs: create_mock_response(
url, "gcp", []
)
result = ray_usage_lib.get_cloud_from_metadata_requests()
assert result == "gcp"
mock_get.side_effect = lambda url, **kwargs: create_mock_response(
url, "aws", []
)
result = ray_usage_lib.get_cloud_from_metadata_requests()
assert result == "aws"
mock_get.side_effect = lambda url, **kwargs: create_mock_response(
url, "azure", []
)
result = ray_usage_lib.get_cloud_from_metadata_requests()
assert result == "azure"
mock_get.side_effect = lambda url, **kwargs: create_mock_response(
url, "", ["gcp", "aws", "azure"]
)
result = ray_usage_lib.get_cloud_from_metadata_requests()
assert result == "unknown"
def test_get_cloud_azure_not_detected_as_aws():
"""Regression test for bug where Azure VMs were incorrectly detected as AWS.
The bug occurred because:
1. AWS endpoint was checked before Azure endpoint
2. Both use the same IP (169.254.169.254)
3. Azure IMDS returns 400 (not 404) when queried with AWS endpoint
4. Old code accepted any non-404 status, so it incorrectly returned "aws"
This test ensures only 200 status is accepted.
"""
with patch("requests.get") as mock_get:
# Simulate being on an Azure VM
# - Azure endpoint returns 200 (correct provider)
# - AWS endpoint returns 400 (Azure IMDS rejecting AWS query)
# - GCP endpoint times out (not on GCP)
def azure_vm_response(url, **kwargs):
mock_response = Mock()
if url == "http://169.254.169.254/metadata/instance?api-version=2021-12-13":
# Azure endpoint succeeds
mock_response.status_code = 200
elif url == "http://169.254.169.254/latest/meta-data/":
# AWS endpoint fails with 400 on Azure IMDS (the critical bug case)
mock_response.status_code = 400
elif url == "http://metadata.google.internal/computeMetadata/v1":
# GCP times out
raise requests.exceptions.ConnectionError()
return mock_response
mock_get.side_effect = azure_vm_response
result = ray_usage_lib.get_cloud_from_metadata_requests()
# Should correctly identify as Azure (not AWS!)
assert result == "azure", (
"Azure VM incorrectly detected as AWS! "
"This is the critical bug where status_code != 404 accepted "
"Azure's 400 response to AWS query."
)
if __name__ == "__main__":
sys.exit(pytest.main(["-sv", __file__]))