|
9 | 9 | import time |
10 | 10 | from functools import wraps |
11 | 11 | from pathlib import Path |
12 | | -from typing import Any, Dict, List, Optional |
| 12 | +from typing import Any, AsyncGenerator, Dict, List, Optional |
13 | 13 |
|
14 | 14 | from commonwealth.utils.apis import GenericErrorHandlingRoute, PrettyJSONResponse |
15 | 15 | from commonwealth.utils.logs import InterceptHandler, init_logger |
16 | 16 | from commonwealth.utils.sentry_config import init_sentry_async |
| 17 | +from commonwealth.utils.streaming import streamer |
17 | 18 | from fastapi import APIRouter, FastAPI, HTTPException, Query, status |
| 19 | +from fastapi.responses import StreamingResponse |
18 | 20 | from fastapi_versioning import VersionedFastAPI, versioned_api_route |
19 | 21 | from loguru import logger |
20 | 22 | from pydantic import BaseModel, Field |
@@ -56,6 +58,12 @@ class DiskSpeedResult(BaseModel): |
56 | 58 | error: Optional[str] = Field(None, description="Error message if test failed") |
57 | 59 |
|
58 | 60 |
|
| 61 | +class DiskSpeedTestPoint(BaseModel): |
| 62 | + size_mb: int = Field(..., description="Test size in MB") |
| 63 | + write_speed: Optional[float] = Field(None, description="Write speed in MiB/s") |
| 64 | + read_speed: Optional[float] = Field(None, description="Read speed in MiB/s") |
| 65 | + |
| 66 | + |
59 | 67 | DiskNode.update_forward_refs() |
60 | 68 |
|
61 | 69 |
|
@@ -303,20 +311,8 @@ def parse_disktest_speed(output: str) -> tuple[Optional[float], Optional[float], |
303 | 311 | return write_speed, read_speed, seed |
304 | 312 |
|
305 | 313 |
|
306 | | -# pylint: disable=too-many-locals |
307 | | -@disk_router.get( |
308 | | - "/speed", |
309 | | - response_model=DiskSpeedResult, |
310 | | - summary="Run disk speed test using disktest binary.", |
311 | | -) |
312 | | -@to_http_exception |
313 | | -async def disk_speed( |
314 | | - size_bytes: int = Query( |
315 | | - 1024 * 1024 * 1024, |
316 | | - ge=1024 * 1024, |
317 | | - description="Number of bytes to test (default 1 GiB).", |
318 | | - ), |
319 | | -) -> DiskSpeedResult: |
| 314 | +async def run_single_speed_test(size_bytes: int) -> DiskSpeedResult: |
| 315 | + """Run a single disk speed test and return the result.""" |
320 | 316 | disktest_binary = "disktest" |
321 | 317 | temp_file_path: Optional[Path] = None |
322 | 318 |
|
@@ -408,6 +404,58 @@ async def disk_speed( |
408 | 404 | logger.warning(f"Failed to clean up temporary file {temp_file_path}: {e}") |
409 | 405 |
|
410 | 406 |
|
| 407 | +# pylint: disable=too-many-locals |
| 408 | +@disk_router.get( |
| 409 | + "/speed", |
| 410 | + response_model=DiskSpeedResult, |
| 411 | + summary="Run disk speed test using disktest binary.", |
| 412 | +) |
| 413 | +@to_http_exception |
| 414 | +async def disk_speed( |
| 415 | + size_bytes: int = Query( |
| 416 | + 1024 * 1024 * 1024, |
| 417 | + ge=1024 * 1024, |
| 418 | + description="Number of bytes to test (default 1 GiB).", |
| 419 | + ), |
| 420 | +) -> DiskSpeedResult: |
| 421 | + return await run_single_speed_test(size_bytes) |
| 422 | + |
| 423 | + |
| 424 | +async def multi_size_speed_test_generator() -> AsyncGenerator[str, None]: |
| 425 | + """Generator that runs speed tests at multiple sizes and yields JSON results.""" |
| 426 | + import json |
| 427 | + |
| 428 | + test_sizes_mb = [10, 50, 100, 200] |
| 429 | + |
| 430 | + for size_mb in test_sizes_mb: |
| 431 | + size_bytes = size_mb * 1024 * 1024 |
| 432 | + result = await run_single_speed_test(size_bytes) |
| 433 | + |
| 434 | + point = DiskSpeedTestPoint( |
| 435 | + size_mb=size_mb, |
| 436 | + write_speed=result.write_speed_mbps, |
| 437 | + read_speed=result.read_speed_mbps, |
| 438 | + ) |
| 439 | + yield json.dumps(point.dict()) |
| 440 | + |
| 441 | + |
| 442 | +@disk_router.get( |
| 443 | + "/speed/stream", |
| 444 | + summary="Run multi-size disk speed test with streaming results.", |
| 445 | +) |
| 446 | +async def disk_speed_stream() -> StreamingResponse: |
| 447 | + return StreamingResponse( |
| 448 | + streamer(multi_size_speed_test_generator(), heartbeats=1.0), |
| 449 | + media_type="application/x-ndjson", |
| 450 | + headers={ |
| 451 | + "Content-Type": "application/x-ndjson", |
| 452 | + "Cache-Control": "no-cache", |
| 453 | + "Connection": "keep-alive", |
| 454 | + "X-Accel-Buffering": "no", |
| 455 | + }, |
| 456 | + ) |
| 457 | + |
| 458 | + |
411 | 459 | fast_api_app = FastAPI( |
412 | 460 | title="Disk Usage API", |
413 | 461 | description="Inspect disk usage and delete files using du.", |
|
0 commit comments