diff --git a/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/version-1.0.x/Release Notes/index.md b/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/version-1.0.x/Release Notes/index.md index 7507bdb10..0539e1053 100644 --- a/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/version-1.0.x/Release Notes/index.md +++ b/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/version-1.0.x/Release Notes/index.md @@ -5,3 +5,4 @@ sidebar_position: 1 * [release v1.0.1](v1.0.1.md) * [release v1.0.2](v1.0.2.md) * [release v1.0.3](v1.0.3.md) +* [release v1.0.4](v1.0.4.md) diff --git a/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/version-1.0.x/Release Notes/v1.0.4.md b/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/version-1.0.x/Release Notes/v1.0.4.md new file mode 100644 index 000000000..5eb95cf41 --- /dev/null +++ b/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/version-1.0.x/Release Notes/v1.0.4.md @@ -0,0 +1,32 @@ +# v1.0.4 + +## 发布日期 +2026 年 2 月 4 日 + +## 概览 +本次发布主要涉及Admin的bug修复。 + +--- + +## Admin + +### Bug 修复 + +修复SandboxActor未汇报namespace指标的问题 + +--- + +## 已知问题 + +* 目前暂无 + + +## 贡献者 + +感谢所有为本次发布做出贡献的贡献者! + +## 下一步计划 + +* 增强文档与教程 + +* 进一步的性能优化与 Bug 修复 \ No newline at end of file diff --git a/docs/versioned_docs/version-1.0.x/Release Notes/index.md b/docs/versioned_docs/version-1.0.x/Release Notes/index.md index 1ee36f7d6..ee1923a6d 100644 --- a/docs/versioned_docs/version-1.0.x/Release Notes/index.md +++ b/docs/versioned_docs/version-1.0.x/Release Notes/index.md @@ -5,3 +5,4 @@ sidebar_position: 1 * [release v1.0.1](v1.0.1.md) * [release v1.0.2](v1.0.2.md) * [release v1.0.3](v1.0.3.md) +* [release v1.0.4](v1.0.4.md) diff --git a/docs/versioned_docs/version-1.0.x/Release Notes/v1.0.4.md b/docs/versioned_docs/version-1.0.x/Release Notes/v1.0.4.md new file mode 100644 index 000000000..0b45800f2 --- /dev/null +++ b/docs/versioned_docs/version-1.0.x/Release Notes/v1.0.4.md @@ -0,0 +1,27 @@ +# v1.0.2 + +## Release Date +February 4, 2026 + +## Overview +This release primarily focuses on Admin bug fix. + +--- + +## Admin + +### Bug Fixes + +Fix the issue that the namespace metric is not reported by SandboxActor. + +--- + +## Known Issues +- None at this time + +## Contributors +Thanks to all the contributors who made this release possible! + +## Next Steps +- Enhance documentation and tutorials +- Further performance optimizations and bug fixes \ No newline at end of file diff --git a/rock/actions/sandbox/response.py b/rock/actions/sandbox/response.py index 54e405f78..e218b566a 100644 --- a/rock/actions/sandbox/response.py +++ b/rock/actions/sandbox/response.py @@ -44,6 +44,7 @@ class SandboxStatusResponse(BaseModel): swe_rex_version: str | None = None user_id: str | None = None experiment_id: str | None = None + namespace: str | None = None cpus: float | None = None memory: str | None = None diff --git a/rock/admin/metrics/decorator.py b/rock/admin/metrics/decorator.py index 108cc70f6..57f2ef1f5 100644 --- a/rock/admin/metrics/decorator.py +++ b/rock/admin/metrics/decorator.py @@ -46,14 +46,16 @@ async def _get_user_info(redis_provider: RedisProvider, sandbox_id: str): if user_info is not None and len(user_info) > 0: user_id = user_info[0].get("user_id") experiment_id = user_info[0].get("experiment_id") + namespace = user_info[0].get("namespace") return ( user_id if user_id is not None else "default", experiment_id if experiment_id is not None else "default", + namespace if namespace is not None else "default", ) - return "default", "default" + return "default", "default", "default" -def _build_attributes(op_name: str, sandbox_id: str, f, user_id: str, experiment_id: str): +def _build_attributes(op_name: str, sandbox_id: str, f, user_id: str, experiment_id: str, namespace: str): """Build attributes for metrics""" return { "operation": op_name, @@ -61,6 +63,7 @@ def _build_attributes(op_name: str, sandbox_id: str, f, user_id: str, experiment "method": f.__name__, "user_id": user_id, "experiment_id": experiment_id, + "namespace": namespace, } @@ -77,7 +80,7 @@ def _record_metrics(metrics_monitor: MetricsMonitor, result, attributes: dict, s """Record metrics after function execution""" # Update sandbox_id from result if available attributes = _update_sandbox_id_from_result(result, attributes) - + # Record success or failure if isinstance(result, Exception): error_attrs = {**attributes, "error_type": type(result).__name__} @@ -85,12 +88,12 @@ def _record_metrics(metrics_monitor: MetricsMonitor, result, attributes: dict, s raise result else: metrics_monitor.record_counter_by_name(f"{metric_prefix}.success", 1, attributes) - + # Record response time and total requests rt_ms = (time.perf_counter() - start_time) * 1000 metrics_monitor.record_gauge_by_name(f"{metric_prefix}.rt", rt_ms, attributes) metrics_monitor.record_counter_by_name(f"{metric_prefix}.total", 1, attributes) - + return result @@ -125,10 +128,10 @@ async def wrapper(self, *args, **kwargs): ) redis_provider: RedisProvider = getattr(self, "_redis_provider", None) - user_id, experiment_id = await _get_user_info(redis_provider, sandbox_id) + user_id, experiment_id, namespace = await _get_user_info(redis_provider, sandbox_id) # Build attributes - attributes = _build_attributes(op_name, sandbox_id, f, user_id, experiment_id) + attributes = _build_attributes(op_name, sandbox_id, f, user_id, experiment_id, namespace) start_time = time.perf_counter() @@ -159,10 +162,10 @@ def wrapper(self, *args, **kwargs): redis_provider: RedisProvider = getattr(self, "_redis_provider", None) # For sync functions, we need to run the async function in a blocking way - user_id, experiment_id = asyncio.run(_get_user_info(redis_provider, sandbox_id)) + user_id, experiment_id, namespace = asyncio.run(_get_user_info(redis_provider, sandbox_id)) # Build attributes - attributes = _build_attributes(op_name, sandbox_id, f, user_id, experiment_id) + attributes = _build_attributes(op_name, sandbox_id, f, user_id, experiment_id, namespace) start_time = time.perf_counter() diff --git a/rock/sandbox/base_actor.py b/rock/sandbox/base_actor.py index 6940c5ad8..e78496a83 100644 --- a/rock/sandbox/base_actor.py +++ b/rock/sandbox/base_actor.py @@ -33,6 +33,7 @@ class BaseActor: _env: str = "dev" _user_id: str = "default" _experiment_id: str = "default" + _namespace = "default" def __init__( self, @@ -150,6 +151,7 @@ async def _collect_sandbox_metrics(self, sandbox_id: str): attributes = {"sandbox_id": sandbox_id, "env": self._env, "role": self._role, "host": self.host} attributes["user_id"] = self._user_id attributes["experiment_id"] = self._experiment_id + attributes["namespace"] = self._namespace self._gauges["cpu"].set(metrics["cpu"], attributes=attributes) self._gauges["mem"].set(metrics["mem"], attributes=attributes) self._gauges["disk"].set(metrics["disk"], attributes=attributes) diff --git a/rock/sdk/sandbox/client.py b/rock/sdk/sandbox/client.py index 48e23d01a..3278b46e9 100644 --- a/rock/sdk/sandbox/client.py +++ b/rock/sdk/sandbox/client.py @@ -854,6 +854,8 @@ def _add_user_defined_tag_into_headers(self, headers: dict): headers["X-User-Id"] = self.config.user_id if self.config.experiment_id: headers["X-Experiment-Id"] = self.config.experiment_id + if self.config.namespace: + headers["X-Namespace"] = self.config.namespace def _is_token_expired(self) -> bool: try: diff --git a/rock/sdk/sandbox/config.py b/rock/sdk/sandbox/config.py index e485a5feb..7109d8c57 100644 --- a/rock/sdk/sandbox/config.py +++ b/rock/sdk/sandbox/config.py @@ -37,6 +37,7 @@ class SandboxConfig(BaseConfig): user_id: str | None = None experiment_id: str | None = None cluster: str = "zb" + namespace: str | None = None class SandboxGroupConfig(SandboxConfig): diff --git a/tests/unit/admin/metrics/test_decorator.py b/tests/unit/admin/metrics/test_decorator.py index 53228ea89..ccf9ccf04 100644 --- a/tests/unit/admin/metrics/test_decorator.py +++ b/tests/unit/admin/metrics/test_decorator.py @@ -1,14 +1,14 @@ import asyncio -from unittest.mock import Mock, patch, AsyncMock +from unittest.mock import AsyncMock, Mock, patch import pytest from rock.admin.metrics.decorator import ( + _build_attributes, _extract_sandbox_id, _get_user_info, - _build_attributes, + _record_metrics, _update_sandbox_id_from_result, - _record_metrics ) from rock.admin.metrics.monitor import MetricsMonitor from rock.utils.providers import RedisProvider @@ -16,6 +16,7 @@ class SampleObject: """测试对象类,用于模拟具有特定属性的对象""" + def __init__(self, sandbox_id=None, container_name=None): self.sandbox_id = sandbox_id self.container_name = container_name @@ -24,11 +25,9 @@ def __init__(self, sandbox_id=None, container_name=None): def test_extract_sandbox_id_with_custom_extractor(): def custom_extractor(*args, **kwargs): return "custom-sandbox-id" - + result = _extract_sandbox_id( - args=("arg1", "arg2"), - kwargs={"param1": "value1"}, - extract_sandbox_id=custom_extractor + args=("arg1", "arg2"), kwargs={"param1": "value1"}, extract_sandbox_id=custom_extractor ) assert result == "custom-sandbox-id" @@ -36,129 +35,128 @@ def custom_extractor(*args, **kwargs): # 测试从kwargs中提取sandbox_id def test_extract_sandbox_id_with_param(): result = _extract_sandbox_id( - args=("arg1", "arg2"), - kwargs={"sandbox_id": "param-sandbox-id"}, - sandbox_id_param="sandbox_id" + args=("arg1", "arg2"), kwargs={"sandbox_id": "param-sandbox-id"}, sandbox_id_param="sandbox_id" ) assert result == "param-sandbox-id" + def test_extract_sandbox_id_with_position(): - result = _extract_sandbox_id( - args=("arg1", "position-sandbox-id", "arg3"), - kwargs={}, - sandbox_id_position=2 - ) + result = _extract_sandbox_id(args=("arg1", "position-sandbox-id", "arg3"), kwargs={}, sandbox_id_position=2) assert result == "position-sandbox-id" + def test_extract_sandbox_id_with_first_arg_string(): result = _extract_sandbox_id(args=("first-sandbox-id", "arg2"), kwargs={}) assert result == "first-sandbox-id" + def test_extract_sandbox_id_with_first_arg_object(): test_obj = SampleObject(container_name="object-sandbox-id") result = _extract_sandbox_id(args=(test_obj, "arg2"), kwargs={}) assert result == "object-sandbox-id" + def test_extract_sandbox_id_prefers_container_name_over_sandbox_id(): test_obj = SampleObject(container_name="container-name", sandbox_id="sandbox-id") result = _extract_sandbox_id(args=(test_obj, "arg2"), kwargs={}) assert result == "container-name" -@patch('rock.admin.metrics.decorator.alive_sandbox_key') + +@patch("rock.admin.metrics.decorator.alive_sandbox_key") def test_get_user_info_success(mock_alive_key): mock_redis_provider = Mock(spec=RedisProvider) - + async def async_mock_return_value(*args, **kwargs): - return [{"user_id": "user123", "experiment_id": "exp456"}] - + return [{"user_id": "user123", "experiment_id": "exp456", "namespace": "ns789"}] + mock_alive_key.return_value = "alive:test-sandbox" mock_redis_provider.json_get = AsyncMock(side_effect=async_mock_return_value) - + # Run the async function in a blocking way - user_id, experiment_id = asyncio.run(_get_user_info(mock_redis_provider, "test-sandbox")) + user_id, experiment_id, namespace = asyncio.run(_get_user_info(mock_redis_provider, "test-sandbox")) assert user_id == "user123" assert experiment_id == "exp456" + assert namespace == "ns789" -@patch('rock.admin.metrics.decorator.alive_sandbox_key') + +@patch("rock.admin.metrics.decorator.alive_sandbox_key") def test_get_user_info_no_data(mock_alive_key): - mock_metrics_monitor = Mock(spec=MetricsMonitor) mock_redis_provider = Mock(spec=RedisProvider) - + async def async_mock_return_value(*args, **kwargs): return [] - + mock_alive_key.return_value = "alive:test-sandbox" mock_redis_provider.json_get = AsyncMock(side_effect=async_mock_return_value) - + # Run the async function in a blocking way - user_id, experiment_id = asyncio.run(_get_user_info(mock_redis_provider, "test-sandbox")) + user_id, experiment_id, namespace = asyncio.run(_get_user_info(mock_redis_provider, "test-sandbox")) assert user_id == "default" assert experiment_id == "default" + assert namespace == "default" + def test_build_attributes(): mock_func = Mock() mock_func.__name__ = "test_function" - - attributes = _build_attributes("test_operation", "test-sandbox", mock_func, "user123", "exp456") - + + attributes = _build_attributes("test_operation", "test-sandbox", mock_func, "user123", "exp456", "ns789") + expected = { "operation": "test_operation", "sandbox_id": "test-sandbox", "method": "test_function", "user_id": "user123", - "experiment_id": "exp456" + "experiment_id": "exp456", + "namespace": "ns789", } assert attributes == expected + def test_update_sandbox_id_from_result(): - mock_metrics_monitor = Mock(spec=MetricsMonitor) mock_result = Mock() mock_result.sandbox_id = "result-sandbox-id" attributes = {"sandbox_id": "original-sandbox-id"} - + updated_attributes = _update_sandbox_id_from_result(mock_result, attributes) assert updated_attributes["sandbox_id"] == "result-sandbox-id" + def test_record_metrics_success(): mock_metrics_monitor = Mock(spec=MetricsMonitor) attributes = {"operation": "test_op"} start_time = 0 result = "success" - + # Mock time.perf_counter to return a fixed value for testing - with patch('rock.admin.metrics.decorator.time.perf_counter', return_value=1.0): + with patch("rock.admin.metrics.decorator.time.perf_counter", return_value=1.0): try: _record_metrics(mock_metrics_monitor, result, attributes, start_time, "test") except Exception: pass # We expect this to return the result, not raise - + # Verify success counter was called - mock_metrics_monitor.record_counter_by_name.assert_any_call( - "test.success", 1, attributes - ) - + mock_metrics_monitor.record_counter_by_name.assert_any_call("test.success", 1, attributes) + # Verify gauge and total counter were called assert mock_metrics_monitor.record_gauge_by_name.called - mock_metrics_monitor.record_counter_by_name.assert_any_call( - "test.total", 1, attributes - ) + mock_metrics_monitor.record_counter_by_name.assert_any_call("test.total", 1, attributes) + def test_record_metrics_failure(): mock_metrics_monitor = Mock(spec=MetricsMonitor) attributes = {"operation": "test_op"} start_time = 0 exception = Exception("test error") - + # Mock time.perf_counter to return a fixed value for testing - with patch('rock.admin.metrics.decorator.time.perf_counter', return_value=1.0): + with patch("rock.admin.metrics.decorator.time.perf_counter", return_value=1.0): with pytest.raises(Exception) as context: _record_metrics(mock_metrics_monitor, exception, attributes, start_time, "test") - + assert str(context.value) == "test error" - + # Verify failure counter was called with error attributes error_attrs = {**attributes, "error_type": "Exception"} - mock_metrics_monitor.record_counter_by_name.assert_called_with( - "test.failure", 1, error_attrs - ) + mock_metrics_monitor.record_counter_by_name.assert_called_with("test.failure", 1, error_attrs)