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__]))