New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[serve] Stabilize metrics pusher #38349
Changes from all commits
56bc3d9
9b3dd84
cc87bde
34a1749
6a405b2
1d823cd
facee76
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,7 @@ | |
import sys | ||
import tempfile | ||
from copy import deepcopy | ||
import time | ||
from unittest.mock import patch | ||
|
||
import numpy as np | ||
|
@@ -24,7 +25,9 @@ | |
dict_keys_snake_to_camel_case, | ||
get_all_live_placement_group_names, | ||
get_head_node_id, | ||
MetricsPusher, | ||
) | ||
from ray.serve.tests.utils import MockTimer | ||
from ray._private.resource_spec import HEAD_NODE_RESOURCE_NAME | ||
|
||
|
||
|
@@ -665,6 +668,87 @@ def test_get_all_live_placement_group_names(ray_instance): | |
assert set(get_all_live_placement_group_names()) == {"pg3", "pg4", "pg5", "pg6"} | ||
|
||
|
||
def test_metrics_pusher_no_tasks(): | ||
"""Test that a metrics pusher can't be started with zero tasks.""" | ||
metrics_pusher = MetricsPusher() | ||
with pytest.raises(ValueError): | ||
metrics_pusher.start() | ||
|
||
|
||
def test_metrics_pusher_basic(): | ||
start = 0 | ||
timer = MockTimer(start) | ||
|
||
with patch("time.time", new=timer.time), patch( | ||
"time.sleep", new=timer.realistic_sleep | ||
): | ||
counter = {"val": 0} | ||
result = {} | ||
expected_result = 20 | ||
|
||
def task(c, res): | ||
timer.realistic_sleep(0.001) | ||
c["val"] += 1 | ||
# At 10 seconds, this task should have been called 20 times | ||
if timer.time() >= 10 and "val" not in res: | ||
res["val"] = c["val"] | ||
|
||
metrics_pusher = MetricsPusher() | ||
metrics_pusher.register_task(lambda: task(counter, result), 0.5) | ||
|
||
metrics_pusher.start() | ||
# This busy wait loop should run for at most a few hundred milliseconds | ||
# The test should finish by then, and if the test fails this prevents | ||
# an infinite loop | ||
for _ in range(10000000): | ||
if "val" in result: | ||
assert result["val"] == expected_result | ||
break | ||
|
||
assert result["val"] == expected_result | ||
|
||
|
||
def test_metrics_pusher_multiple_tasks(): | ||
start = 0 | ||
timer = MockTimer(start) | ||
|
||
with patch("time.time", new=timer.time), patch( | ||
"time.sleep", new=timer.realistic_sleep | ||
): | ||
counter = {"A": 0, "B": 0, "C": 0} | ||
result = {} | ||
expected_results = {"A": 35, "B": 14, "C": 10} | ||
|
||
def task(key, c, res): | ||
time.sleep(0.001) | ||
c[key] += 1 | ||
# Check for how many times this task has been called | ||
# At 7 seconds, tasks A, B, C should have executed 35, 14, and 10 | ||
# times respectively. | ||
if timer.time() >= 7 and key not in res: | ||
res[key] = c[key] | ||
|
||
metrics_pusher = MetricsPusher() | ||
# Each task interval is different, and they don't divide each other. | ||
metrics_pusher.register_task(lambda: task("A", counter, result), 0.2) | ||
metrics_pusher.register_task(lambda: task("B", counter, result), 0.5) | ||
metrics_pusher.register_task(lambda: task("C", counter, result), 0.7) | ||
metrics_pusher.start() | ||
|
||
# This busy wait loop should run for at most a few hundred milliseconds | ||
# The test should finish by then, and if the test fails this prevents | ||
# an infinite loop | ||
for _ in range(10000000): | ||
for key in result.keys(): | ||
assert result[key] == expected_results[key] | ||
if len(result) == 3: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should the test fail if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah good call, I've added this in both tests! |
||
break | ||
|
||
# Check there are three results set and all are expected. | ||
for key in expected_results.keys(): | ||
assert result[key] == expected_results[key] | ||
|
||
|
||
if __name__ == "__main__": | ||
import sys | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import time | ||
from typing import Any | ||
|
||
|
||
class MockTimer: | ||
def __init__(self, start_time=None): | ||
if start_time is None: | ||
start_time = time.time() | ||
self._curr = start_time | ||
|
||
def time(self): | ||
return self._curr | ||
|
||
def advance(self, by): | ||
self._curr += by | ||
|
||
def realistic_sleep(self, amt): | ||
self._curr += amt + 0.001 | ||
|
||
|
||
class MockKVStore: | ||
def __init__(self): | ||
self.store = dict() | ||
|
||
def put(self, key: str, val: Any) -> bool: | ||
if not isinstance(key, str): | ||
raise TypeError("key must be a string, got: {}.".format(type(key))) | ||
self.store[key] = val | ||
return True | ||
|
||
def get(self, key: str) -> Any: | ||
if not isinstance(key, str): | ||
raise TypeError("key must be a string, got: {}.".format(type(key))) | ||
return self.store.get(key, None) | ||
|
||
def delete(self, key: str) -> bool: | ||
if not isinstance(key, str): | ||
raise TypeError("key must be a string, got: {}.".format(type(key))) | ||
|
||
if key in self.store: | ||
del self.store[key] | ||
return True | ||
|
||
return False |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Nit] Could we raise an error if the
MetricsPusher
is started without any tasks registered? As written, it silently sleeps forever sinceleast_interval_s
gets set tomath.inf
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That makes sense! I've added this to
MetricsPusher.start()