Skip to content

fix: correct defects surfaced by a docstring audit - #1243

Open
ajcasagrande wants to merge 1 commit into
mainfrom
ajc/docstring-audit-code
Open

fix: correct defects surfaced by a docstring audit#1243
ajcasagrande wants to merge 1 commit into
mainfrom
ajc/docstring-audit-code

Conversation

@ajcasagrande

@ajcasagrande ajcasagrande commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Summary

A docstring audit surfaced places where a comment disagreed with the code because the code was wrong. This PR carries those fixes — 41 files, each defect reproduced with a failing test before being fixed.

Companion to #1242 (the comment-only half). The two touch no files in common and can merge in either order.

Reports that did not reproduce were left alone rather than "fixed" — several turned out to be intentional, and two would have caused regressions if changed (see below).

Defects fixed

Defect Impact
PromptGenerator wraparound re-appended at most one corpus Silently returned fewer than num_tokens whenever num_tokens > 2 * corpus_size
huggingface_generate dispatched on the global endpoint.streaming flag, not payload shape Silently dropped a streamed token when a per-request stream override disagreed
SlimRecord.endpoint_latency_ns required a non-optional int to_slim() raised ValidationError on a live path — data_collector passes None when no aiohttp trace timing exists
get_level_number did getattr(cls, level.upper()) Raised AttributeError for every string level name
Custom GPU telemetry fields colliding on a derived internal_name Emitted duplicate metrics, including collisions with built-in defaults
Histogram timeslices starting before the first sample Emitted with a fabricated zero baseline instead of being skipped
network_latency shutdown slept on Environment.SERVER_METRICS.SHUTDOWN_DELAY Governed by an unrelated subsystem's env var; its own settings class had no such field
get_typed_metadata's hand-maintained category mapping Silently returned raw dicts for 5 categories; now derived from categories.yaml so it cannot drift again
cancel_all_tasks accepted a timeout it never used Cancelled tasks were never awaited, so callers returned before cleanup ran
Operator-mode rejection message pointed at docs/kubernetes/sweeps.md That path does not exist

Also: rankings composer skipped _finalize_conversations; sagemaker loader skipped per-load state resets, reusing the previous file's trace_id; empty-file and unsupported-format paths returned None or raised UnboundLocalError; topic-only ZMQ frames fed an empty payload to the decoder; unguarded all_nodes_view dereference before mount; platform.system() replaced with IS_MACOS per project convention.

Choices worth a reviewer's attention

  • SlimRecord.endpoint_latency_ns made optional rather than defaulting to 0 in to_slim() — the sole consumer already guards None, and a synthetic zero would silently skew latency statistics.
  • Pre-first-sample timeslices are skipped, not zero-filled. For a sliced series a fabricated zero is worse than an absent slice; _compute_histogram_stats' fallback exists because whole-range stats must return something.
  • cancel_all_tasks excludes the current task from its wait set — it is called from inside managed tasks on the shutdown path, and awaiting self would deadlock. A genuinely uncancellable task is used in the test to prove the bounded return.
  • The rankings fix does not change the wire payload. base_rankings_endpoint.format_payload reads only query/passages. This makes the Conversation objects and exported inputs.json consistent with every sibling composer; carrying a system prompt in a rankings request would need an endpoint-side change.
  • Two regression guards were added deliberately: the plugin metadata test now enumerates declaring categories from the registry rather than a hardcoded list, and the operator-message test asserts every docs/ path named in the message resolves on disk. Both catch the next instance of the bug they guard rather than the one instance found.

Deliberately not changed

  • Three socket-option constants in http_defaults.py are dead, not missing a setsockoptSO_LINGER takes a struct linger (not an int), and SO_REUSEADDR/SO_REUSEPORT only affect bind() on a socket that is never bound. Adding the calls would change connection behavior at scale for no benefit.
  • A sticky_router pop() ordering hazard is unreachable today (no await between selection and tracking, single-threaded asyncio) but is one refactor away from routing a credit to None. Flagged rather than changed on a hot path.

Verification

  • uv run pytest tests/unit/ -n auto17730 passed, 0 failed
  • ruff check / ruff format --check clean
  • Generated CLI docs (15 commands, 585 params) and env-var docs (26 subsystems, 167 vars) in sync
  • 35 plugin categories and 261 plugins validate

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added configurable network-latency shutdown delay, defaulting to five seconds.
    • Synthetic ranking datasets now include configured context prompts.
    • Oversized prompt requests now generate the full requested token count.
  • Bug Fixes

    • Improved Hugging Face response handling for streaming, full, and empty responses.
    • Prevented shutdown hangs, duplicate telemetry metrics, invalid histogram slices, and dashboard errors during startup.
    • Added clearer errors for empty datasets and unsupported audio formats.
    • Empty messages are safely ignored.
  • Documentation

    • Updated environment-variable guidance and sweep instructions.

A docstring audit surfaced places where a comment disagreed with the code
because the CODE was wrong. Each was reproduced with a failing test before
being fixed; reported issues that did not reproduce were left alone.

User-facing defects:
- PromptGenerator wraparound re-appended at most one corpus, silently
  returning fewer than num_tokens whenever num_tokens > 2 * corpus_size. Now
  loops; byte-identical for inputs that wrapped at most once.
- huggingface_generate parsed responses by the global endpoint.streaming flag
  rather than the payload shape, silently dropping a streamed token whenever a
  per-request stream override disagreed. Now dispatches on payload shape,
  matching the sibling endpoints.
- ServerMetricsRecord.endpoint_latency_ns is int | None and data_collector
  passes None when no aiohttp trace timing exists, but SlimRecord required a
  non-optional int, so to_slim() raised ValidationError on a live path.
  SlimRecord's field is now optional; the sole consumer already guards None,
  and a synthetic 0 would have skewed latency stats.
- get_level_number raised AttributeError for every string level name.
- Custom GPU telemetry fields colliding on a derived internal_name emitted
  duplicate metrics, including collisions with built-in defaults.
- Histogram timeslices starting before the first sample were emitted with a
  fabricated zero baseline instead of being skipped.
- network_latency shutdown slept on Environment.SERVER_METRICS.SHUTDOWN_DELAY;
  its own settings class had no such field. Added SHUTDOWN_DELAY to
  _NetworkLatencySettings and regenerated the env-var docs.
- An operator-mode rejection message pointed at docs/kubernetes/sweeps.md,
  which does not exist. Its test asserts every docs path named in the message
  resolves on disk, so the next stale pointer fails rather than shipping.

Robustness and correctness:
- cancel_all_tasks accepted a timeout it never used and never awaited the
  tasks it cancelled. Now bounded-waits, excluding the current task so a call
  from inside a managed task cannot deadlock.
- get_typed_metadata silently returned raw dicts for five categories missing
  from a hand-maintained mapping. The mapping is now derived from
  categories.yaml so it cannot drift again, and its test enumerates the
  declaring categories from the registry rather than a hardcoded list.
- Rankings composer skipped _finalize_conversations, so system and user-context
  prompts never reached the composed conversations. Note this makes the
  Conversation objects consistent with every sibling composer but does not
  change the rankings wire payload, which reads only query/passages.
- sagemaker loader skipped per-load state resets, reusing the previous file's
  trace_id across back-to-back loads.
- Empty-file and unsupported-format paths returned None or raised
  UnboundLocalError into consumers expecting neither.
- Topic-only ZMQ frames fed an empty payload to the decoder.
- Unguarded all_nodes_view dereference before mount.
- platform.system() replaced with IS_MACOS per project convention.

Not changed, reported instead: three socket-option constants in http_defaults
that are dead rather than missing a setsockopt (SO_LINGER takes a struct
linger; the other two only affect bind() on a socket that is never bound), and
a sticky_router pop() ordering hazard that is unreachable today but is one
refactor away from routing a credit to None.

Verified: 17730 unit tests pass, ruff clean, generated CLI and env-var docs in
sync, plugin schemas validate.

Signed-off-by: Anthony Casagrande <acasagrande@nvidia.com>
@ajcasagrande

Copy link
Copy Markdown
Contributor Author

Companion: #1242 carries the comment-only half. No files in common; either order is fine.

@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown

Try out this PR

Quick install:

pip install --upgrade --force-reinstall git+https://github.com/ai-dynamo/aiperf.git@224533bd6420223537ae0df5f4a31373989daebf

Recommended with virtual environment (using uv):

uv venv --python 3.12 && source .venv/bin/activate
uv pip install --upgrade --force-reinstall git+https://github.com/ai-dynamo/aiperf.git@224533bd6420223537ae0df5f4a31373989daebf

Last updated for commit: 224533bBrowse code

@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The pull request updates runtime configuration, task cancellation, dataset handling, endpoint parsing, telemetry metadata, plugin resolution, server-metrics export, and related regression tests and documentation.

Changes

Runtime and data-path corrections

Layer / File(s) Summary
Configuration and runtime behavior
docs/environment-variables.md, src/aiperf/cli_runner/*, src/aiperf/common/environment.py, src/aiperf/network_latency/manager.py, tests/unit/cli_runner/*, tests/unit/network_latency/*
Adds the network-latency shutdown delay setting, uses shared macOS detection, and updates CLI exit-code and documentation guidance.
Common contracts and task lifecycle
src/aiperf/common/aiperf_logger.py, src/aiperf/common/mixins/*, src/aiperf/common/models/*, src/aiperf/common/protocols.py, tests/unit/common/*
Updates log-level parsing, task cancellation, telemetry metadata, slim-record typing, and protocol references.
Dataset validation and generation
src/aiperf/dataset/*, tests/unit/dataset/*
Rejects empty files and unsupported audio formats, finalizes synthetic conversations, wraps oversized prompts, and resets loader state between loads.
Endpoint and message handling
src/aiperf/endpoints/*, src/aiperf/ui/dashboard/*, src/aiperf/zmq/*, tests/unit/endpoints/*, tests/unit/ui/*, tests/unit/zmq/*
Dispatches Hugging Face responses by shape, supports empty SOLIDO results, guards dashboard updates before view creation, and drops empty ZMQ payloads.
Telemetry and plugin metadata
src/aiperf/gpu_telemetry/metrics_config.py, src/aiperf/plugin/plugins.py, tests/unit/gpu_telemetry/*, tests/unit/plugin/*
Skips duplicate custom metric names and resolves plugin metadata classes from category declarations.
Server-metrics slicing and export
src/aiperf/server_metrics/*, tests/unit/server_metrics/*
Corrects histogram slice boundaries, supports open-start histogram filters, and aligns export documentation with runtime errors and return types.

Estimated code review effort: 4 (Complex) | ~45 minutes

Poem

I’m a rabbit with tests in my paws,
I hop through metrics, queues, and laws.
Empty frames now fade from sight,
Prompts wrap round to the requested height.
Config blooms, parsers choose their way—
Snuffle-snuffle, cleaner code today!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 78.49% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the pull request as fixes for defects identified by a docstring audit.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (4)
src/aiperf/dataset/generator/audio.py (1)

180-185: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use AudioFormat without .value.

Line 181 accesses a string enum through .value. Build supported_formats from the direct string enum representation. Preserve wav and mp3 in the error message.

As per coding guidelines, “Use string-based enums through MessageType.X directly; never use .value.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aiperf/dataset/generator/audio.py` around lines 180 - 185, Update the
unsupported-format branch in the audio generator to build supported_formats from
each AudioFormat member directly rather than accessing .value, while preserving
the existing sorted list and ensuring wav and mp3 remain in the error message.

Source: Coding guidelines

tests/unit/dataset/composer/test_custom_composer.py (1)

569-580: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add type hints to the changed test methods.

Each changed test method lacks type hints for its fixture parameters and its None return value.

  • tests/unit/dataset/composer/test_custom_composer.py#L569-L580: Add fixture parameter types and -> None.
  • tests/unit/dataset/composer/test_custom_composer.py#L582-L593: Add fixture parameter types and -> None.
  • tests/unit/dataset/composer/test_synthetic_rankings_composer.py#L112-L123: Add fixture parameter types and -> None.
  • tests/unit/dataset/composer/test_synthetic_rankings_composer.py#L126-L137: Add fixture parameter types and -> None.
  • tests/unit/dataset/generator/test_audio_generator.py#L294-L301: Add -> None.
  • tests/unit/dataset/generator/test_prompt_generator.py#L450-L466: Add fixture and mock parameter types and -> None.
  • tests/unit/dataset/generator/test_prompt_generator.py#L468-L481: Add fixture parameter types and -> None.
  • tests/unit/dataset/loader/test_can_load.py#L405-L412: Add fixture parameter types and -> None.
  • tests/unit/dataset/loader/test_sagemaker_data_capture.py#L905-L914: Add tmp_path type and -> None.
  • tests/unit/dataset/loader/test_sagemaker_data_capture.py#L916-L923: Add tmp_path type and -> None.

As per coding guidelines, “Add type hints to every function parameter and return value.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/dataset/composer/test_custom_composer.py` around lines 569 - 580,
Annotate every parameter and return value in the affected tests:
tests/unit/dataset/composer/test_custom_composer.py lines 569-580 and 582-593;
tests/unit/dataset/composer/test_synthetic_rankings_composer.py lines 112-123
and 126-137; tests/unit/dataset/generator/test_audio_generator.py lines 294-301;
tests/unit/dataset/generator/test_prompt_generator.py lines 450-466 and 468-481;
tests/unit/dataset/loader/test_can_load.py lines 405-412; and
tests/unit/dataset/loader/test_sagemaker_data_capture.py lines 905-914 and
916-923. Use the existing fixture and mock annotation conventions in each test
module, add the appropriate type for tmp_path and other fixture parameters, and
declare each test method with a None return type.

Source: Coding guidelines

tests/unit/endpoints/test_huggingface_generate.py (2)

108-134: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the prescribed parametrization format.

The new decorators use raw booleans and place # fmt: skip on the decorator line. The test guidelines require from pytest import param and the formatter directive on the closing ) line.

As per coding guidelines, use param(...) for parametrized cases and place # fmt: skip on the closing ) line.

Proposed change
+from pytest import param
+
-    `@pytest.mark.parametrize`("global_streaming", [True, False])  # fmt: skip
+    `@pytest.mark.parametrize`(
+        "global_streaming",
+        [param(True), param(False)],
+    )  # fmt: skip
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/endpoints/test_huggingface_generate.py` around lines 108 - 134,
Update both parametrization decorators in
test_parse_response_stream_event_shape_calls_streaming and
test_parse_response_full_body_shape_calls_non_streaming to use pytest.param via
the prescribed from pytest import param import, and move # fmt: skip to the
closing parenthesis line of each decorator. Preserve the existing True and False
test cases.

Source: Coding guidelines


109-167: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add type hints to each new test method.

The same type-hint gap appears across these changed test methods. Add annotations to fixture parameters and -> None to each method.

  • tests/unit/endpoints/test_huggingface_generate.py#L109-L167: annotate each new test method's parameters and return value.
  • tests/unit/endpoints/test_solido_rag.py#L301-L303: annotate endpoint and add -> None.
  • tests/unit/ui/test_realtime_telemetry_dashboard.py#L656-L663: annotate the method parameters and add -> None.
  • tests/unit/zmq/test_sub_client.py#L195-L208: annotate the fixture parameters and add -> None.

As per coding guidelines, every Python function parameter and return value must have a type hint.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/endpoints/test_huggingface_generate.py` around lines 109 - 167,
Annotate every changed test method with type hints: add appropriate fixture
parameter types and -> None in
tests/unit/endpoints/test_huggingface_generate.py:109-167,
tests/unit/endpoints/test_solido_rag.py:301-303,
tests/unit/ui/test_realtime_telemetry_dashboard.py:656-663, and
tests/unit/zmq/test_sub_client.py:195-208. Preserve each test’s existing
behavior and use the established fixture types in the surrounding test modules.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/aiperf/common/mixins/task_manager_mixin.py`:
- Around line 59-73: Update cancel_all_tasks to resolve the current task before
the cancellation loop and exclude it from both cancellation and asyncio.wait()
via the existing current-task comparison. Preserve sibling cancellation and
timeout handling, and add a regression test where a managed task invokes
cancel_all_tasks while a sibling completes its cancellation cleanup.

In `@src/aiperf/ui/dashboard/realtime_telemetry_dashboard.py`:
- Around line 311-315: Update the telemetry handler around all_nodes_view and
compose so non-empty metrics received before mounting are replayed after the
view is created. Track an explicit rendered-state sentinel rather than using
self.metrics to decide whether visibility and rendering have occurred, ensuring
the mounted view becomes visible and receives the cached list. Add a regression
test in test_realtime_telemetry_dashboard.py covering a non-empty pre-compose
metrics list.

In `@tests/unit/gpu_telemetry/test_metrics_config.py`:
- Around line 458-483: Add the explicit -> None return annotation to both test
methods, test_build_custom_metrics_from_csv_colliding_internal_names_dedupes and
test_build_custom_metrics_from_csv_collision_with_default_internal_name_skipped,
without changing their existing test behavior.

In `@tests/unit/server_metrics/test_timeslices.py`:
- Around line 1196-1220: Add the missing return annotation to
test_histogram_timeslices_slices_before_first_sample_are_skipped, declaring that
the test returns None. Leave the test logic unchanged.

---

Nitpick comments:
In `@src/aiperf/dataset/generator/audio.py`:
- Around line 180-185: Update the unsupported-format branch in the audio
generator to build supported_formats from each AudioFormat member directly
rather than accessing .value, while preserving the existing sorted list and
ensuring wav and mp3 remain in the error message.

In `@tests/unit/dataset/composer/test_custom_composer.py`:
- Around line 569-580: Annotate every parameter and return value in the affected
tests: tests/unit/dataset/composer/test_custom_composer.py lines 569-580 and
582-593; tests/unit/dataset/composer/test_synthetic_rankings_composer.py lines
112-123 and 126-137; tests/unit/dataset/generator/test_audio_generator.py lines
294-301; tests/unit/dataset/generator/test_prompt_generator.py lines 450-466 and
468-481; tests/unit/dataset/loader/test_can_load.py lines 405-412; and
tests/unit/dataset/loader/test_sagemaker_data_capture.py lines 905-914 and
916-923. Use the existing fixture and mock annotation conventions in each test
module, add the appropriate type for tmp_path and other fixture parameters, and
declare each test method with a None return type.

In `@tests/unit/endpoints/test_huggingface_generate.py`:
- Around line 108-134: Update both parametrization decorators in
test_parse_response_stream_event_shape_calls_streaming and
test_parse_response_full_body_shape_calls_non_streaming to use pytest.param via
the prescribed from pytest import param import, and move # fmt: skip to the
closing parenthesis line of each decorator. Preserve the existing True and False
test cases.
- Around line 109-167: Annotate every changed test method with type hints: add
appropriate fixture parameter types and -> None in
tests/unit/endpoints/test_huggingface_generate.py:109-167,
tests/unit/endpoints/test_solido_rag.py:301-303,
tests/unit/ui/test_realtime_telemetry_dashboard.py:656-663, and
tests/unit/zmq/test_sub_client.py:195-208. Preserve each test’s existing
behavior and use the established fixture types in the surrounding test modules.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: cb8ba3b9-ce36-4fcc-bf0b-9165fc7fff14

📥 Commits

Reviewing files that changed from the base of the PR and between c2f5e9d and 224533b.

📒 Files selected for processing (41)
  • docs/environment-variables.md
  • src/aiperf/cli_runner/_multi_run.py
  • src/aiperf/cli_runner/_process_setup.py
  • src/aiperf/common/aiperf_logger.py
  • src/aiperf/common/environment.py
  • src/aiperf/common/mixins/task_manager_mixin.py
  • src/aiperf/common/models/server_metrics_models.py
  • src/aiperf/common/models/telemetry_models.py
  • src/aiperf/common/protocols.py
  • src/aiperf/dataset/composer/custom.py
  • src/aiperf/dataset/composer/synthetic_rankings.py
  • src/aiperf/dataset/generator/audio.py
  • src/aiperf/dataset/generator/prompt.py
  • src/aiperf/dataset/loader/sagemaker_data_capture.py
  • src/aiperf/endpoints/huggingface_generate.py
  • src/aiperf/endpoints/solido_rag.py
  • src/aiperf/gpu_telemetry/metrics_config.py
  • src/aiperf/network_latency/manager.py
  • src/aiperf/plugin/plugins.py
  • src/aiperf/server_metrics/export_stats.py
  • src/aiperf/server_metrics/parquet_exporter.py
  • src/aiperf/ui/dashboard/realtime_telemetry_dashboard.py
  • src/aiperf/zmq/sub_client.py
  • tests/unit/cli_runner/test_multi_run_operator_message.py
  • tests/unit/cli_runner/test_process_setup_platform.py
  • tests/unit/common/test_docstring_audit_bugs.py
  • tests/unit/dataset/composer/test_custom_composer.py
  • tests/unit/dataset/composer/test_synthetic_rankings_composer.py
  • tests/unit/dataset/generator/test_audio_generator.py
  • tests/unit/dataset/generator/test_prompt_generator.py
  • tests/unit/dataset/loader/test_can_load.py
  • tests/unit/dataset/loader/test_sagemaker_data_capture.py
  • tests/unit/endpoints/test_huggingface_generate.py
  • tests/unit/endpoints/test_solido_rag.py
  • tests/unit/gpu_telemetry/test_metrics_config.py
  • tests/unit/network_latency/test_manager.py
  • tests/unit/plugin/test_orchestrator_categories.py
  • tests/unit/server_metrics/test_parquet_exporter.py
  • tests/unit/server_metrics/test_timeslices.py
  • tests/unit/ui/test_realtime_telemetry_dashboard.py
  • tests/unit/zmq/test_sub_client.py

Comment on lines +59 to +73
# Never await ourselves: cancel_all_tasks can be invoked from inside one of
# the managed tasks, and awaiting the current task would deadlock.
current = asyncio.current_task()
pending = [
task for task in task_list if not task.done() and task is not current
]
if not pending:
return

_, still_pending = await asyncio.wait(pending, timeout=timeout)
if still_pending:
self.warning(
lambda: f"{len(still_pending)} task(s) did not finish within "
f"{timeout}s of cancellation; abandoning them."
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Do not cancel the calling task.

The loop cancels every task before Lines 61-64 exclude the current task from asyncio.wait(). If a managed task calls cancel_all_tasks(), its cancellation is delivered at Line 68. The method then exits before it waits for sibling cleanup.

Resolve current before the cancellation loop. Exclude it from both cancellation and waiting. Add a regression test where a managed task calls cancel_all_tasks() and a sibling task performs cancellation cleanup.

Proposed fix
+        current = asyncio.current_task()
         task_list = list(self.tasks)
         for task in task_list:
-            task.cancel()
+            if task is not current:
+                task.cancel()

-        current = asyncio.current_task()
         pending = [
             task for task in task_list if not task.done() and task is not current
         ]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Never await ourselves: cancel_all_tasks can be invoked from inside one of
# the managed tasks, and awaiting the current task would deadlock.
current = asyncio.current_task()
pending = [
task for task in task_list if not task.done() and task is not current
]
if not pending:
return
_, still_pending = await asyncio.wait(pending, timeout=timeout)
if still_pending:
self.warning(
lambda: f"{len(still_pending)} task(s) did not finish within "
f"{timeout}s of cancellation; abandoning them."
)
current = asyncio.current_task()
task_list = list(self.tasks)
for task in task_list:
if task is not current:
task.cancel()
# Never await ourselves: cancel_all_tasks can be invoked from inside one of
# the managed tasks, and awaiting the current task would deadlock.
pending = [
task for task in task_list if not task.done() and task is not current
]
if not pending:
return
_, still_pending = await asyncio.wait(pending, timeout=timeout)
if still_pending:
self.warning(
lambda: f"{len(still_pending)} task(s) did not finish within "
f"{timeout}s of cancellation; abandoning them."
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aiperf/common/mixins/task_manager_mixin.py` around lines 59 - 73, Update
cancel_all_tasks to resolve the current task before the cancellation loop and
exclude it from both cancellation and asyncio.wait() via the existing
current-task comparison. Preserve sibling cancellation and timeout handling, and
add a regression test where a managed task invokes cancel_all_tasks while a
sibling completes its cancellation cleanup.

Comment on lines +311 to +315
if self.all_nodes_view is None:
# Metrics can arrive before compose() runs (or after teardown); the
# view is created in compose, so there is nothing to update yet.
self.metrics = metrics
return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Replay cached telemetry after the view is created.

When a non-empty metrics list reaches Lines [311]-[315] before compose() runs, the handler stores it and returns. compose() creates all_nodes_view but does not render the cached list. A later update also skips the visibility block because self.metrics is already non-empty. The status message can remain visible while the node view remains hidden.

Replay cached metrics after the view is mounted. Track whether the view has been rendered instead of using self.metrics as the first-update sentinel. Add a regression case with a non-empty pre-compose list and verify that the view becomes visible and receives the list. The current test in tests/unit/ui/test_realtime_telemetry_dashboard.py passes only [] and does not cover this path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aiperf/ui/dashboard/realtime_telemetry_dashboard.py` around lines 311 -
315, Update the telemetry handler around all_nodes_view and compose so non-empty
metrics received before mounting are replayed after the view is created. Track
an explicit rendered-state sentinel rather than using self.metrics to decide
whether visibility and rendering have occurred, ensuring the mounted view
becomes visible and receives the cached list. Add a regression test in
test_realtime_telemetry_dashboard.py covering a non-empty pre-compose metrics
list.

Comment on lines +458 to +483
def test_build_custom_metrics_from_csv_colliding_internal_names_dedupes(self):
"""Two custom DCGM fields whose derived internal_name collides must not
both be emitted as metrics — the second one is skipped."""
csv_content = """DCGM_FI_DEV_CUSTOM_THING, gauge, Custom Thing (in W)
DCGM_FI_PROF_CUSTOM_THING, gauge, Custom Thing Prof (in W)
"""
with tempfile.NamedTemporaryFile(
mode="w", suffix=".csv", delete=False, encoding="utf-8"
) as f:
f.write(csv_content)
csv_path = Path(f.name)

try:
loader = MetricsConfigLoader()
custom_metrics, new_dcgm_mappings = loader.build_custom_metrics_from_csv(
custom_csv_path=csv_path
)

internal_names = [m[1] for m in custom_metrics]
assert internal_names == ["nvidia_custom_thing"]
assert len(internal_names) == len(set(internal_names))
assert new_dcgm_mappings == {
"DCGM_FI_DEV_CUSTOM_THING": "nvidia_custom_thing"
}
finally:
csv_path.unlink()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add return annotations to both test methods.

Add -> None to test_build_custom_metrics_from_csv_colliding_internal_names_dedupes and test_build_custom_metrics_from_csv_collision_with_default_internal_name_skipped.

As per coding guidelines, “Add type hints to every function parameter and return value.”

Also applies to: 485-509

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/gpu_telemetry/test_metrics_config.py` around lines 458 - 483, Add
the explicit -> None return annotation to both test methods,
test_build_custom_metrics_from_csv_colliding_internal_names_dedupes and
test_build_custom_metrics_from_csv_collision_with_default_internal_name_skipped,
without changing their existing test behavior.

Source: Coding guidelines

Comment on lines +1196 to +1220
def test_histogram_timeslices_slices_before_first_sample_are_skipped(self):
ts = ServerMetricsTimeSeries()
# First sample lands at t=3s; the filter starts at t=0.
add_histogram_snapshots(
ts,
"latency",
[
(3 * NANOS_PER_SECOND, hist({"1.0": 1.0, "+Inf": 2.0}, 20.0, 2.0)),
(4 * NANOS_PER_SECOND, hist({"1.0": 2.0, "+Inf": 4.0}, 40.0, 4.0)),
(5 * NANOS_PER_SECOND, hist({"1.0": 3.0, "+Inf": 6.0}, 60.0, 6.0)),
],
)

slices = _compute_histogram_timeslices(
get_histogram(ts, "latency"),
slice_duration=1.0,
time_filter=make_time_filter(start_ns=0, end_ns=5 * NANOS_PER_SECOND),
)

assert slices is not None
# [0-1), [1-2), [2-3) all end before the first sample at t=3s.
assert all(s.start_ns >= 3 * NANOS_PER_SECOND for s in slices), (
f"Slices before first sample were emitted: "
f"{[(s.start_ns, s.count) for s in slices]}"
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add the missing return type.

Line 1196 defines a new test without a return annotation. Add -> None.

Proposed fix
-    def test_histogram_timeslices_slices_before_first_sample_are_skipped(self):
+    def test_histogram_timeslices_slices_before_first_sample_are_skipped(self) -> None:

As per coding guidelines, “Add type hints to every function parameter and return value.”

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_histogram_timeslices_slices_before_first_sample_are_skipped(self):
ts = ServerMetricsTimeSeries()
# First sample lands at t=3s; the filter starts at t=0.
add_histogram_snapshots(
ts,
"latency",
[
(3 * NANOS_PER_SECOND, hist({"1.0": 1.0, "+Inf": 2.0}, 20.0, 2.0)),
(4 * NANOS_PER_SECOND, hist({"1.0": 2.0, "+Inf": 4.0}, 40.0, 4.0)),
(5 * NANOS_PER_SECOND, hist({"1.0": 3.0, "+Inf": 6.0}, 60.0, 6.0)),
],
)
slices = _compute_histogram_timeslices(
get_histogram(ts, "latency"),
slice_duration=1.0,
time_filter=make_time_filter(start_ns=0, end_ns=5 * NANOS_PER_SECOND),
)
assert slices is not None
# [0-1), [1-2), [2-3) all end before the first sample at t=3s.
assert all(s.start_ns >= 3 * NANOS_PER_SECOND for s in slices), (
f"Slices before first sample were emitted: "
f"{[(s.start_ns, s.count) for s in slices]}"
)
def test_histogram_timeslices_slices_before_first_sample_are_skipped(self) -> None:
ts = ServerMetricsTimeSeries()
# First sample lands at t=3s; the filter starts at t=0.
add_histogram_snapshots(
ts,
"latency",
[
(3 * NANOS_PER_SECOND, hist({"1.0": 1.0, "+Inf": 2.0}, 20.0, 2.0)),
(4 * NANOS_PER_SECOND, hist({"1.0": 2.0, "+Inf": 4.0}, 40.0, 4.0)),
(5 * NANOS_PER_SECOND, hist({"1.0": 3.0, "+Inf": 6.0}, 60.0, 6.0)),
],
)
slices = _compute_histogram_timeslices(
get_histogram(ts, "latency"),
slice_duration=1.0,
time_filter=make_time_filter(start_ns=0, end_ns=5 * NANOS_PER_SECOND),
)
assert slices is not None
# [0-1), [1-2), [2-3) all end before the first sample at t=3s.
assert all(s.start_ns >= 3 * NANOS_PER_SECOND for s in slices), (
f"Slices before first sample were emitted: "
f"{[(s.start_ns, s.count) for s in slices]}"
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/server_metrics/test_timeslices.py` around lines 1196 - 1220, Add
the missing return annotation to
test_histogram_timeslices_slices_before_first_sample_are_skipped, declaring that
the test returns None. Leave the test logic unchanged.

Source: Coding guidelines

@codecov

codecov Bot commented Aug 1, 2026

Copy link
Copy Markdown

@debermudez debermudez left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review summary

Solid, well-structured set of fixes. Every one of the 10+ defects is correctly diagnosed and fixed, and each has a regression test that exercises the failure mode rather than just the happy path. The cancel_all_tasks await fix and the get_typed_metadata registry derivation are particularly nice. Two things to address:

  1. Minor: time.sleep(0.5) in test_cancel_all_tasks_uncancellable_task_returns_within_timeout — CLAUDE.md forbids time.sleep. The intent is sound (simulating an uncancellable thread), but a threading.Event avoids the 0.5s real wall-clock cost and the daemon thread leak.
  2. Cleanup: src/aiperf/dataset/generator/video.py:46 still calls platform.system() raw — one line that could be fixed here since the PR is already touching this convention in _process_setup.py.

Happy to approve once those two are cleaned up.

async def _uncancellable() -> None:
# to_thread cannot be interrupted by cancellation until the thread returns.
await asyncio.to_thread(time.sleep, 0.5)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

time.sleep violates CLAUDE.md — a threading.Event avoids the real 0.5s cost.

The intent is correct: asyncio.to_thread can't be interrupted by asyncio cancellation until the thread returns, so this genuinely simulates an uncancellable task. But time.sleep(0.5) is real wall-clock time — the auto-fixture only mocks asyncio.sleep — and the daemon thread continues sleeping after the test returns.

Alternative that blocks without sleeping:

async def _uncancellable() -> None:
    done = threading.Event()
    try:
        await asyncio.to_thread(done.wait)
    finally:
        done.set()

The finally fires when the coroutine is cancelled, which sets the event and lets the thread exit immediately.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants