Skip to content

Decoupling Metric Store from Worker Coordinator - #988

Open
ajleong623 wants to merge 103 commits into
opensearch-project:mainfrom
ajleong623:memory-leaks
Open

Decoupling Metric Store from Worker Coordinator#988
ajleong623 wants to merge 103 commits into
opensearch-project:mainfrom
ajleong623:memory-leaks

Conversation

@ajleong623

@ajleong623 ajleong623 commented Nov 18, 2025

Copy link
Copy Markdown
Contributor

Description

There is a new actor called SamplePostProcessorActor. However the SamplePostProcessorActor acts as an actor for processing samples directly from the Worker actors as well as any task involving the metrics store. '

SamplePostProcessorActor initialization:

The new actor is initialized in the coordinator. This happens in the prepare_benchmark method. The SamplePostProcessorActor is initialized after receiving the StartSamplePostProcessorActor message which has the configurations for creating the metrics store, the metrics sample and profile sample post processor objects, and the telemetry collectors. StartTelemetry and StopTelemetry messages are now used to start and stop the telemetry collection threads through the coordinator

Which methods were changed:

  • send_samples: In the worker class, instead of just sending samples to the coordinator, a ProcessSamples message is sent to the new SamplePostProcessorActor which is then sent to the SamplePostprocessor instance inside of the SamplePostProcessorActor.
  • to_externalizable: In the metric store, the to_externalizable method is used to send the results of running the workload to the coordinator. We will now need to call the method through the SamplePostProcessorActor and the GetExternalizableMetricsStore message.
  • close: When the coordinator is closed, the message CloseMetricsStore will now be used to signal to the new actor to close the metrics store.
  • reset_relative_time: The ResetRelativeTimeRequest message will now be used instead to reset the relative time of the metric store in the new actor.

One of the concerns if with synchronization. A lot of methods involving the metrics store that were synchronous are now handled through an asynchronous message to the SamplePostProcessorActor which holds the metric store.

Issues Resolved

[List any issues this PR will resolve]

Testing

  • New functionality includes testing

[Describe how this change was tested]


By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
For more information on following Developer Certificate of Origin and signing off your commits, please check here.

Signed-off-by: Anthony Leong <aj.leong623@gmail.com>
Signed-off-by: Anthony Leong <aj.leong623@gmail.com>
Signed-off-by: Anthony Leong <aj.leong623@gmail.com>
Signed-off-by: Anthony Leong <aj.leong623@gmail.com>
Signed-off-by: Anthony Leong <aj.leong623@gmail.com>
@coderabbitai

coderabbitai Bot commented Feb 3, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • 🔍 Trigger a full review
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Important

Action Needed: IP Allowlist Update

If your organization protects your Git platform with IP whitelisting, please add the new CodeRabbit IP address to your allowlist:

  • 136.113.208.247/32 (new)
  • 34.170.211.100/32
  • 35.222.179.152/32

Failure to add the new IP will result in interrupted reviews.


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

@github-actions

github-actions Bot commented Mar 16, 2026

Copy link
Copy Markdown

PR Code Analyzer ❗

AI-powered 'Code-Diff-Analyzer' found issues on commit f954800.

PathLineSeverityDescription
osbenchmark/worker_coordinator/worker_coordinator.py1090mediumThe prepare_telemetry refactor into SamplePostProcessorActor removes the database_type check that previously skipped OpenSearch-specific telemetry for non-OpenSearch databases. All telemetry devices are now unconditionally added when enabled, regardless of database type. This is likely an unintentional regression rather than malicious, but warrants review.

The table above displays the top 10 most important findings.

Total: 1 | Critical: 0 | High: 0 | Medium: 1 | Low: 0


Pull Requests Author(s): Please update your Pull Request according to the report above.

Repository Maintainer(s): You can bypass diff analyzer by adding label skip-diff-analyzer after reviewing the changes carefully, then re-run failed actions. To re-enable the analyzer, remove the label, then re-run all actions.


⚠️ Note: The Code-Diff-Analyzer helps protect against potentially harmful code patterns. Please ensure you have thoroughly reviewed the changes beforehand.

Thanks.

@github-actions

github-actions Bot commented Mar 16, 2026

Copy link
Copy Markdown

PR Reviewer Guide 🔍

(Review updated until commit a3bf0c9)

Here are some key observations to aid the review process:

🧪 PR contains tests
🔒 No security concerns identified
✅ No TODO sections
🔀 No multiple PR themes
⚡ Recommended focus areas for review

Argument Order Bug

TaskBoundaryFlushed is constructed with positional args (metric_results, msg.waiting_period, msg.workers_curr_step), but the class __init__ signature is (self, metrics, next_task_scheduled_in, workers_curr_step). This means waiting_period is assigned to next_task_scheduled_in (correct) and workers_curr_step to workers_curr_step (correct). However, verify: the handler receiveMsg_TaskBoundaryFlushed calls self.on_task_finished(msg.metrics, msg.next_task_scheduled_in) and self.coordinator.drive_workers_for_next_task(msg.workers_curr_step, msg.next_task_scheduled_in) — the values line up but the naming is confusing. The bigger risk is that waiting_period is passed where a scheduled-in time is expected downstream; ensure this matches prior semantics where on_task_finished received waiting_period as the second argument.

self.send(self.worker_coordinator_actor, TaskBoundaryFlushed(metric_results, msg.waiting_period, msg.workers_curr_step))
Race on ActorExitRequest

In receiveMsg_BenchmarkCancelled, an ActorExitRequest is sent to sample_post_processor_actor, but self.coordinator.close() was already called earlier which sends CloseMetricsStore to the same actor via close_metric_store(). The CloseMetricsStore message may still be queued when ActorExitRequest is processed, or vice versa; also close()/receiveMsg_ActorExitRequest both invoke self.close() — while it's guarded by self.closed, the actor still tries to post_process_samples and stop_telemetry twice in the second call, which raises on metrics_store=None. Only the first close is safe; the idempotency check should be earlier or handle stale references more defensively.

def receiveMsg_BenchmarkCancelled(self, msg, sender):
    self.logger.info("Main worker_coordinator received a notification that the benchmark has been cancelled.")
    self.status = "exiting"
    self.coordinator.close()
    if self.sample_post_processor_actor is not None:
        self.logger.info("Shutting down SamplePostProcessorActor due to benchmark cancellation.")
        self.send(self.sample_post_processor_actor, thespian.actors.ActorExitRequest())
Ordering Break

send_samples sends ProcessSamples and then FlushAndForwardJoinPoint in sequence. However, receiveMsg_FlushAndForwardJoinPoint forwards the JoinPointReached to the worker coordinator BEFORE calling post_process_samples(). This means the coordinator can advance to the next task (or trigger final flush) before the last batch of ProcessSamples has been drained. Since actor mailboxes are FIFO per sender, this may still work for the sample buffer, but coordinator-side actions like FlushAndClose or FlushForTaskBoundary may be enqueued after the join point forwarding and race with the still-buffered samples in some paths.

def receiveMsg_FlushAndForwardJoinPoint(self, msg, sender):
    self.logger.debug("Forwarding join point before sample post-processing attempt.")
    self.send(self.worker_coordinator_actor, msg.joinpoint_reached)
    try:
        self.post_process_samples()
    except BaseException as e:
        self.logger.exception("Could not post-process samples after forwarding join point.")
        self.send(self.worker_coordinator_actor, actor.BenchmarkFailure("Error in sample post processor ({})".format(str(e))))
Possible Issue

receiveMsg_BenchmarkComplete calls self.on_benchmark_complete(msg.metrics), but this handler is now defined on WorkerCoordinatorActor and receives a BenchmarkComplete message from SamplePostProcessorActor. Previously, on_benchmark_complete was invoked directly by the coordinator with the externalized metrics. Confirm that on_benchmark_complete still exists as a method on WorkerCoordinatorActor (not shown in diff) and that no code path additionally invokes it, otherwise it may be called twice or not at all.

def receiveMsg_BenchmarkComplete(self, msg, sender):
    self.on_benchmark_complete(msg.metrics)

@github-actions

github-actions Bot commented Mar 16, 2026

Copy link
Copy Markdown

PR Code Suggestions ✨

Latest suggestions up to a3bf0c9

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Guard against missing post-processor actor

When joinpoint_reached is provided but there are no samples and no
sample_post_processor_actor has been set (e.g., initial join point before
StartWorker completed), sending FlushAndForwardJoinPoint to None will raise. Guard
the send with a check that self.sample_post_processor_actor is not None, or route
the join-point back through self.master in that case to avoid losing the join-point
signal.

osbenchmark/worker_coordinator/worker_coordinator.py [2113-2134]

 def send_samples(self, joinpoint_reached=None):
     if self.sampler:
         samples = self.sampler.samples
     else:
         samples = []
     if self.profile_sampler:
         profile_samples = self.profile_sampler.samples
     else:
         profile_samples = []
+    if self.sample_post_processor_actor is None and joinpoint_reached is not None:
+        self.send(self.master, joinpoint_reached)
+        return samples
Suggestion importance[1-10]: 6

__

Why: Reasonable defensive check for a potential edge case where sample_post_processor_actor may be None, though in practice StartWorker sets it before drive() is called. Still, guarding against a None send target improves robustness.

Low
Verify message argument ordering consistency

The TaskBoundaryFlushed message is constructed as
TaskBoundaryFlushed(metric_results, msg.waiting_period, msg.workers_curr_step) where
the second positional arg maps to next_task_scheduled_in and the third to
workers_curr_step. However, when handled here, msg.next_task_scheduled_in receives
waiting_period and msg.workers_curr_step receives the workers dict — verify the
argument ordering matches, since a mismatch would send incorrect scheduling data to
workers. Also confirm the tests' expectation sent_msg.next_task_scheduled_in == 5,
sent_msg.workers_curr_step == workers_curr_step is consistent with the constructor
signature TaskBoundaryFlushed(metrics, next_task_scheduled_in, workers_curr_step).

osbenchmark/worker_coordinator/worker_coordinator.py [773-774]

+self.on_task_finished(msg.metrics, msg.next_task_scheduled_in)
+self.coordinator.drive_workers_for_next_task(msg.workers_curr_step, msg.next_task_scheduled_in)
 
-
Suggestion importance[1-10]: 3

__

Why: The suggestion only asks the user to verify argument ordering without proposing an actual change (existing_code equals improved_code). The ordering appears consistent between the constructor signature and usage.

Low
General
Avoid redundant ISO8601 conversion round-trip

index_name calls osb_time.from_iso8601 on the string just produced by
osb_time.to_iso8601. It's simpler and avoids a redundant round-trip to compute the
index directly from the datetime returned by self.config.opts("system",
"time.start"), which is already a datetime. Also, if time.start is ever changed to
not be a datetime, to_iso8601 will fail; using the datetime directly is more robust.

osbenchmark/worker_coordinator/worker_coordinator.py [1240-1244]

 elif cpu_max and metrics_store_type is metrics.OsMetricsStore:
     # pass over the index and test run ID so the feedbackActor can query the datastore
-    test_run_timestamp = osb_time.to_iso8601(self.config.opts("system", "time.start"))
-    metrics_index = self.index_name(test_run_timestamp)
+    ts = self.config.opts("system", "time.start")
+    metrics_index = "benchmark-metrics-%04d-%02d" % (ts.year, ts.month)
     test_run_id = self.config.opts("system", "test_run.id")
Suggestion importance[1-10]: 5

__

Why: Valid observation that the ISO8601 conversion round-trip is unnecessary since time.start is already a datetime. This is a minor code quality improvement that also reduces coupling.

Low

Previous suggestions

Suggestions up to commit d380153
CategorySuggestion                                                                                                                                    Impact
Possible issue
Guard close against missing post-processor actor

close unconditionally sends a CloseMetricsStore message via
self.target.sample_post_processor_actor, but this attribute may be None (e.g., if
close is invoked before prepare_benchmark completes, such as after an early
failure). This will raise an error inside error-handling paths. Guard against a
missing post-processor actor before sending.

osbenchmark/worker_coordinator/worker_coordinator.py [1366-1371]

 def close(self):
     self.progress_publisher.finish()
     self.close_metric_store()
 
 def close_metric_store(self):
-    self.target.send(self.target.sample_post_processor_actor, CloseMetricsStore())
+    if getattr(self.target, "sample_post_processor_actor", None) is not None:
+        self.target.send(self.target.sample_post_processor_actor, CloseMetricsStore())
Suggestion importance[1-10]: 6

__

Why: Valid concern: if close is called after an early failure before prepare_benchmark completes, sample_post_processor_actor may be None, causing errors in error paths. Adding a guard improves robustness.

Low
Guard against uninitialized post-processor actor

Previously, send_samples returned None when there was no sampler; now it returns an
empty list. Callers using if samples is None semantics may be affected. Also, when
called from drive() at the joinpoint, send_samples now always sends a
FlushAndForwardJoinPoint message even when both samplers are None (initial
joinpoint), which forwards to the coordinator through the post-processor. Ensure
self.sample_post_processor_actor is guaranteed to be set before the first joinpoint;
otherwise self.send(None, ...) will fail.

osbenchmark/worker_coordinator/worker_coordinator.py [2112-2133]

 def send_samples(self, joinpoint_reached=None):
     if self.sampler:
         samples = self.sampler.samples
     else:
         samples = []
     if self.profile_sampler:
         profile_samples = self.profile_sampler.samples
     else:
         profile_samples = []
+    assert self.sample_post_processor_actor is not None, "sample_post_processor_actor must be set before sending samples/joinpoints"
Suggestion importance[1-10]: 3

__

Why: Adding an assert is a weak defensive check; the underlying concern about sample_post_processor_actor being None at initial joinpoint is worth verifying, but the proposed fix only adds an assertion rather than proper handling.

Low
General
Timing drift risk in async task boundary flush

The TaskBoundaryFlushed receiver in WorkerCoordinatorActor calls
self.on_task_finished(msg.metrics, msg.next_task_scheduled_in) then
self.coordinator.drive_workers_for_next_task(msg.workers_curr_step,
msg.next_task_scheduled_in). However, drive_workers_for_next_task uses
waiting_period relative to time.perf_counter() captured at that moment, but the
flush operation may have taken significant time since the workers reached the
joinpoint. This will cause worker start scheduling to drift. Consider capturing
master_received_msg_at reference at joinpoint time and using absolute timestamps for
scheduling instead of a relative perf_counter delta computed after the flush
round-trip.

osbenchmark/worker_coordinator/worker_coordinator.py [214-221]

+class TaskBoundaryFlushed:
+    """
+    Indicates that the metrics store has been flushed and externalized at a task boundary.
+    """
+    def __init__(self, metrics, next_task_scheduled_in, workers_curr_step):
+        self.metrics = metrics
+        self.next_task_scheduled_in = next_task_scheduled_in
+        self.workers_curr_step = workers_curr_step
 
-
Suggestion importance[1-10]: 5

__

Why: The suggestion raises a valid concern about timing drift due to the async round-trip between joinpoint flush and drive scheduling, but the improved_code is identical to the existing_code, providing no concrete fix.

Low
Avoid unnecessary timestamp round-trip conversion

osb_time.to_iso8601 is called to format the timestamp, then index_name immediately
parses it back with from_iso8601. This round-trip is unnecessary and error-prone
since the input is already a datetime. Pass the datetime directly to compute the
index name to avoid a redundant conversion.

osbenchmark/worker_coordinator/worker_coordinator.py [1240-1244]

 elif cpu_max and metrics_store_type is metrics.OsMetricsStore:
     # pass over the index and test run ID so the feedbackActor can query the datastore
-    test_run_timestamp = osb_time.to_iso8601(self.config.opts("system", "time.start"))
-    metrics_index = self.index_name(test_run_timestamp)
+    ts = self.config.opts("system", "time.start")
+    metrics_index = "benchmark-metrics-%04d-%02d" % (ts.year, ts.month)
     test_run_id = self.config.opts("system", "test_run.id")
Suggestion importance[1-10]: 4

__

Why: The suggestion correctly identifies a redundant to_iso8601/from_iso8601 round-trip. The improvement is minor and stylistic, though it slightly improves clarity and efficiency.

Low
Suggestions up to commit 33c6735
CategorySuggestion                                                                                                                                    Impact
Possible issue
Metrics index may miss migration suffix

Previously metrics_index was derived from self.metrics_store.index, which accounted
for the migrated (.new) index name lookup performed in OsMetricsStore.open().
Reconstructing the index name from just the timestamp bypasses that migration check
and may point the FeedbackActor at a non-existent or wrong index. Consider resolving
the actual index name from the metrics store (e.g., by querying the
SamplePostProcessorActor) rather than recomputing it here.

osbenchmark/worker_coordinator/worker_coordinator.py [1240-1244]

 elif cpu_max and metrics_store_type is metrics.OsMetricsStore:
     # pass over the index and test run ID so the feedbackActor can query the datastore
+    # NOTE: this bypasses OsMetricsStore's migrated-index resolution; ensure the index name matches.
     test_run_timestamp = osb_time.to_iso8601(self.config.opts("system", "time.start"))
     metrics_index = self.index_name(test_run_timestamp)
     test_run_id = self.config.opts("system", "test_run.id")
Suggestion importance[1-10]: 7

__

Why: This raises a legitimate concern: previously metrics_store.index reflected the migrated .new index name after OsMetricsStore.open(), but now the index is reconstructed purely from timestamp, potentially pointing to a wrong/missing index for the FeedbackActor CPU-based feedback.

Medium
Handle missing post-processor actor safely

Iterating samples in insertion order and unconditionally overwriting
latest_progress_per_client[s.client_id] means the "latest" value depends on sampler
ordering. If samples are not guaranteed to be chronologically ordered, use a
timestamp comparison, or at minimum document the ordering assumption. Also, when
joinpoint_reached is set but there are no samples, the join point is still forwarded
— good — but ensure the coordinator won't miss the join point if
sample_post_processor_actor is None (e.g., during initial join point before
benchmark setup).

osbenchmark/worker_coordinator/worker_coordinator.py [2112-2133]

 def send_samples(self, joinpoint_reached=None):
-    if self.sampler:
-        samples = self.sampler.samples
-    else:
-        samples = []
-    if self.profile_sampler:
-        profile_samples = self.profile_sampler.samples
-    else:
-        profile_samples = []
+    samples = self.sampler.samples if self.sampler else []
+    profile_samples = self.profile_sampler.samples if self.profile_sampler else []
 
-    # Map client ids to their latest task progress, e.g. {0: (0.5, "%")}.
+    # Map client ids to their latest task progress based on sample ordering.
     latest_progress_per_client = {}
     for s in samples:
         if s.task_progress is not None:
             latest_progress_per_client[s.client_id] = s.task_progress
     if latest_progress_per_client:
         self.send(self.master, UpdateProgressSamples(latest_progress_per_client))
+    if self.sample_post_processor_actor is None:
+        if joinpoint_reached:
+            self.send(self.master, joinpoint_reached)
+        return samples
     if samples or profile_samples:
         self.send(self.sample_post_processor_actor, ProcessSamples(samples, profile_samples))
     if joinpoint_reached:
         self.send(self.sample_post_processor_actor, FlushAndForwardJoinPoint(joinpoint_reached))
     return samples
Suggestion importance[1-10]: 5

__

Why: Adding a fallback path when sample_post_processor_actor is None could prevent lost join points if this actor is not yet initialized. However, in practice the actor is created in prepare_benchmark before workers start, so the risk is likely low.

Low
General
Avoid duplicate close during cancellation

self.coordinator.close() now calls
self.target.send(self.target.sample_post_processor_actor, CloseMetricsStore()), and
immediately after this handler additionally sends an ActorExitRequest to the same
actor. The CloseMetricsStore message will race with ActorExitRequest and may be
processed after the actor has already begun exiting or duplicated (both trigger
close()). Since close() is idempotent this is likely safe, but consider skipping the
CloseMetricsStore send during cancellation to avoid confusing shutdown ordering.

osbenchmark/worker_coordinator/worker_coordinator.py [685-691]

 def receiveMsg_BenchmarkCancelled(self, msg, sender):
     self.logger.info("Main worker_coordinator received a notification that the benchmark has been cancelled.")
     self.status = "exiting"
-    self.coordinator.close()
+    self.coordinator.progress_publisher.finish()
     if self.sample_post_processor_actor is not None:
         self.logger.info("Shutting down SamplePostProcessorActor due to benchmark cancellation.")
         self.send(self.sample_post_processor_actor, thespian.actors.ActorExitRequest())
Suggestion importance[1-10]: 4

__

Why: Valid observation about potential race between CloseMetricsStore and ActorExitRequest, but since close() is idempotent this is mostly cosmetic. The improved code changes to only progress_publisher.finish() which may skip other necessary cleanup.

Low
Use keyword args for message construction

The FlushForTaskBoundary handler constructs TaskBoundaryFlushed(metric_results,
msg.waiting_period, msg.workers_curr_step) — passing waiting_period in the
next_task_scheduled_in position. This is consistent, but the receiver
receiveMsg_TaskBoundaryFlushed calls
drive_workers_for_next_task(msg.workers_curr_step, msg.next_task_scheduled_in),
which is fine. However, verify the argument order matches the constructor — in
receiveMsg_FlushForTaskBoundary the call is TaskBoundaryFlushed(metric_results,
msg.waiting_period, msg.workers_curr_step), which correctly maps positionally. No
change needed, but consider using keyword arguments to prevent future mistakes.

osbenchmark/worker_coordinator/worker_coordinator.py [214-221]

+class TaskBoundaryFlushed:
+    """
+    Indicates that the metrics store has been flushed and externalized at a task boundary.
+    """
+    def __init__(self, metrics, next_task_scheduled_in, workers_curr_step):
+        self.metrics = metrics
+        self.next_task_scheduled_in = next_task_scheduled_in
+        self.workers_curr_step = workers_curr_step
 
-
Suggestion importance[1-10]: 2

__

Why: The suggestion admits "No change needed" and existing_code equals improved_code. It only vaguely recommends future consideration without a concrete improvement.

Low
Suggestions up to commit 27d93db
CategorySuggestion                                                                                                                                    Impact
Possible issue
Fix incorrect datetime.timedelta usage in tests

The test file imports datetime as from datetime import datetime (the class), but the
tests use datetime.timedelta(...), which does not exist on the datetime class. This
will raise AttributeError at test runtime. Import timedelta or the datetime module
instead.

tests/worker_coordinator/worker_coordinator_test.py [2665-2667]

 self.actor.wakeupAfter.assert_called_once_with(
-    datetime.timedelta(seconds=worker_coordinator.WorkerCoordinatorActor.POST_PROCESS_INTERVAL_SECONDS)
+    timedelta(seconds=worker_coordinator.WorkerCoordinatorActor.POST_PROCESS_INTERVAL_SECONDS)
 )
Suggestion importance[1-10]: 8

__

Why: The test file imports from datetime import datetime but calls datetime.timedelta(...), which will raise AttributeError at runtime, causing test failures. This is a valid bug catch.

Medium
General
Reuse index-name logic to avoid divergence

The index name computation here duplicates the logic in OsMetricsStore.index_name()
(which now correctly uses from_iso8601). Consider reusing that method (e.g., a
static/class helper in metrics) to prevent divergence if the index naming scheme
changes.

osbenchmark/worker_coordinator/worker_coordinator.py [1240-1244]

 elif cpu_max and metrics_store_type is metrics.OsMetricsStore:
     # pass over the index and test run ID so the feedbackActor can query the datastore
     test_run_timestamp = osb_time.to_iso8601(self.config.opts("system", "time.start"))
-    metrics_index = self.index_name(test_run_timestamp)
+    metrics_index = metrics.OsMetricsStore.index_name_for(test_run_timestamp)
     test_run_id = self.config.opts("system", "test_run.id")
Suggestion importance[1-10]: 4

__

Why: Refactoring to reuse the index naming logic would reduce duplication, but the suggested helper index_name_for does not exist and would require additional changes; it's a reasonable maintainability improvement.

Low
Guard against missing workers state

The TaskBoundaryFlushed constructor takes arguments in order (metrics,
next_task_scheduled_in, workers_curr_step), but FlushForTaskBoundary handler
constructs it as TaskBoundaryFlushed(metric_results, msg.waiting_period,
msg.workers_curr_step) — which is correct — while msg.next_task_scheduled_in here is
actually the waiting period. This is fine, but ensure the semantic naming is
consistent; also verify msg.workers_curr_step is not None before dereferencing, as
it can be if a failure path occurred.

osbenchmark/worker_coordinator/worker_coordinator.py [769-774]

 @actor.no_retry("worker_coordinator")  # pylint: disable=no-value-for-parameter
 def receiveMsg_TaskBoundaryFlushed(self, msg, sender):
     if self.status == "exiting":
         self.logger.info("Ignoring task-boundary flush because worker coordinator is exiting.")
         return
+    if msg.workers_curr_step is None:
+        self.logger.warning("Received TaskBoundaryFlushed without workers_curr_step; skipping drive.")
+        return
     self.on_task_finished(msg.metrics, msg.next_task_scheduled_in)
     self.coordinator.drive_workers_for_next_task(msg.workers_curr_step, msg.next_task_scheduled_in)
Suggestion importance[1-10]: 3

__

Why: Adding a None guard for msg.workers_curr_step is a minor defensive improvement; in practice the sender always sets this field, so the impact is low.

Low
Avoid blocking actor on client creation

WorkerCoordinator.prepare_benchmark sets self.target.sample_post_processor_actor on
the actor, but in SamplePostProcessorActor.receiveMsg_StartSamplePostProcessorActor,
self.worker_coordinator_actor = sender. Since start_benchmark sends StartTelemetry
via self.target.send(...), the sender seen by the post-processor actor is the
coordinator actor — good. However, create_os_clients is called synchronously during
message handling and may block the actor for a long time. Consider moving cluster
client creation off the actor's message-handling path to prevent stalling other
messages.

osbenchmark/worker_coordinator/worker_coordinator.py [1119-1120]

 self.target.sample_post_processor_actor = self.target.createActor(SamplePostProcessorActor)
-self.target.send(self.target.sample_post_processor_actor, StartSamplePostProcessorActor(self.config, self.workload, self.test_procedure, downsample_factor))
+self.target.send(self.target.sample_post_processor_actor,
+                 StartSamplePostProcessorActor(self.config, self.workload, self.test_procedure, downsample_factor))
Suggestion importance[1-10]: 2

__

Why: The suggestion is vague and does not provide a concrete code change; the improved_code is essentially the same as existing_code, offering little actionable value.

Low
Suggestions up to commit 13c52a3
CategorySuggestion                                                                                                                                    Impact
Possible issue
Index name may miss migration suffix

Previously the metrics-index name was taken from self.metrics_store.index, which
honored index migration via _migrated_index_name (.new suffix if it exists).
Reconstructing the name here with benchmark-metrics-YYYY-MM bypasses that migration
logic, so the FeedbackActor may query a stale/wrong index. Consider retrieving the
actual index name from the metrics store actor (e.g., via a request/response
message) instead of recomputing it locally.

osbenchmark/worker_coordinator/worker_coordinator.py [1238-1249]

-if cpu_max and metrics_store_type is metrics.InMemoryMetricsStore:
-    raise exceptions.SystemSetupError("CPU-based feedback requires a metrics store. You are using an in-memory metrics store")
-elif cpu_max and "node-stats" not in self.config.opts("telemetry", "devices"):
-    raise exceptions.SystemSetupError("Node stats telemetry not enabled — this is required for CPU-based redline feedback.")
 elif cpu_max and metrics_store_type is metrics.OsMetricsStore:
-    # pass over the index and test run ID so the feedbackActor can query the datastore
-    self.logger.info("Time start before conversion: %s", self.config.opts("system", "time.start"))
-    self.logger.info("Time start after conversion: %s", osb_time.to_iso8601(self.config.opts("system", "time.start")))
+    # Request the resolved index name and test_run_id from the SamplePostProcessorActor to honor migrated index names.
     test_run_timestamp = osb_time.to_iso8601(self.config.opts("system", "time.start"))
     metrics_index = self.index_name(test_run_timestamp)
     test_run_id = self.config.opts("system", "test_run.id")
Suggestion importance[1-10]: 7

__

Why: Valid concern: reconstructing the index name locally bypasses the _migrated_index_name logic used by the metrics store, potentially causing the FeedbackActor to query the wrong index.

Medium
General
Guard against missing coordinator reference

receiveMsg_WakeupMessage is invoked before receiveMsg_StartSamplePostProcessorActor
sets up worker_coordinator_actor and schedules the first wakeup. If a stray wakeup
message arrives (or the actor system delivers one prior to Start),
self.worker_coordinator_actor may be undefined, causing AttributeError when a
failure occurs. Initialize self.worker_coordinator_actor = None in init and
guard the failure send.

osbenchmark/worker_coordinator/worker_coordinator.py [1471-1478]

 @actor.no_retry("sample post processor")  # pylint: disable=no-value-for-parameter
 def receiveMsg_WakeupMessage(self, msg, sender):
     if not self.closed:
         try:
             self.post_process_samples()
         except BaseException as e:
             self.logger.exception("Could not post-process samples on periodic wakeup.")
-            self.send(self.worker_coordinator_actor, actor.BenchmarkFailure("Error in sample post processor ({})".format(str(e))))
+            if getattr(self, "worker_coordinator_actor", None) is not None:
+                self.send(self.worker_coordinator_actor, actor.BenchmarkFailure("Error in sample post processor ({})".format(str(e))))
         self.wakeupAfter(datetime.timedelta(seconds=WorkerCoordinatorActor.POST_PROCESS_INTERVAL_SECONDS))
Suggestion importance[1-10]: 5

__

Why: Reasonable defensive coding suggestion; initializing worker_coordinator_actor in __init__ avoids potential AttributeError, though the scenario is unlikely in practice.

Low
Verify argument ordering across task boundary flow

The TaskBoundaryFlushed constructor signature is (metrics, next_task_scheduled_in,
workers_curr_step), but here msg.waiting_period is passed as next_task_scheduled_in.
While the value is semantically correct, verify the field naming stays consistent.
More critically, the test
test_worker_coordinator_actor_drives_workers_after_task_boundary_flushed_reply calls
drive_workers_for_next_task(workers_curr_step, waiting_period) in that order, but
the actor code calls
self.coordinator.drive_workers_for_next_task(msg.workers_curr_step,
msg.next_task_scheduled_in) — ensure the drive_workers_for_next_task signature
matches (workers_curr_step, waiting_period) as defined.

osbenchmark/worker_coordinator/worker_coordinator.py [1508]

+else:
+    self.send(self.worker_coordinator_actor, TaskBoundaryFlushed(metric_results, msg.waiting_period, msg.workers_curr_step))
 
-
Suggestion importance[1-10]: 3

__

Why: The suggestion only asks to verify the argument ordering without proposing a concrete change (existing_code equals improved_code), providing limited actionable value.

Low
Ensure actor message payloads remain picklable

Messages passed between Thespian actors must be picklable. workers_curr_step is a
dict containing time.perf_counter() timestamps (floats) which are fine, but ensure
that when the actor system uses multiprocess transport, nothing non-picklable (e.g.,
locks, generators) is inadvertently added. Also TaskBoundaryFlushed should be
picklable across process boundaries — verify the class is defined at module scope
(it is) and does not accidentally receive complex objects.

osbenchmark/worker_coordinator/worker_coordinator.py [214-221]

 class TaskBoundaryFlushed:
     """
     Indicates that the metrics store has been flushed and externalized at a task boundary.
+    Note: All fields must be picklable for actor system message passing.
     """
     def __init__(self, metrics, next_task_scheduled_in, workers_curr_step):
         self.metrics = metrics
         self.next_task_scheduled_in = next_task_scheduled_in
         self.workers_curr_step = workers_curr_step
Suggestion importance[1-10]: 2

__

Why: The suggestion is only a doc note (comment addition) and doesn't identify a concrete issue in the code.

Low
Suggestions up to commit ab2e077
CategorySuggestion                                                                                                                                    Impact
General
Avoid re-deriving metrics index locally

The index name computed here duplicates logic from OsMetricsStore.index_name() and
reconstructs it from time.start. If the metrics store applies a migrated index name
(e.g., via _migrated_index_name), this local computation will diverge and the
FeedbackActor may query the wrong index. Consider obtaining the actual index and
test_run_id from the SamplePostProcessorActor (which owns the metrics store) instead
of re-deriving them here.

osbenchmark/worker_coordinator/worker_coordinator.py [1235-1248]

 metrics_store_type = metrics.metrics_store_class(self.config)
-self.logger.info("Metrics store type is %s!!!", metrics_store_type)
 if cpu_max and metrics_store_type is metrics.InMemoryMetricsStore:
     raise exceptions.SystemSetupError("CPU-based feedback requires a metrics store. You are using an in-memory metrics store")
 elif cpu_max and "node-stats" not in self.config.opts("telemetry", "devices"):
     raise exceptions.SystemSetupError("Node stats telemetry not enabled — this is required for CPU-based redline feedback.")
 elif cpu_max and metrics_store_type is metrics.OsMetricsStore:
-    # pass over the index and test run ID so the feedbackActor can query the datastore
-    self.logger.info("Time start before conversion: %s", self.config.opts("system", "time.start"))
-    self.logger.info("Time start after conversion: %s", osb_time.to_iso8601(self.config.opts("system", "time.start")))
+    # Retrieve authoritative index/test_run_id from the SamplePostProcessorActor which owns the metrics store,
+    # to avoid divergence with migrated index names.
     test_run_timestamp = osb_time.to_iso8601(self.config.opts("system", "time.start"))
     metrics_index = self.index_name(test_run_timestamp)
     test_run_id = self.config.opts("system", "test_run.id")
-    self.logger.info("New Index name [%s], new test run id [%s]", metrics_index, test_run_id)
Suggestion importance[1-10]: 6

__

Why: Valid concern that reconstructing the index name locally may diverge from the metrics store's actual index (especially with migration logic). This could lead to FeedbackActor querying a wrong index.

Low
Clarify parameter naming consistency

The TaskBoundaryFlushed constructor is called as TaskBoundaryFlushed(metric_results,
msg.waiting_period, msg.workers_curr_step) but the class signature is init(self,
metrics, next_task_scheduled_in, workers_curr_step). This is consistent, but the
handler uses msg.next_task_scheduled_in for the waiting period which matches
waiting_period from FlushForTaskBoundary. Verify that field naming stays consistent
— the constructor stores next_task_scheduled_in but the sender passes
waiting_period; consider renaming for clarity to prevent future confusion.

osbenchmark/worker_coordinator/worker_coordinator.py [772-774]

+@actor.no_retry("worker_coordinator")  # pylint: disable=no-value-for-parameter
+def receiveMsg_TaskBoundaryFlushed(self, msg, sender):
+    self.on_task_finished(msg.metrics, msg.next_task_scheduled_in)
+    self.coordinator.drive_workers_for_next_task(msg.workers_curr_step, msg.next_task_scheduled_in)
 
-
Suggestion importance[1-10]: 2

__

Why: The suggestion is essentially a "verify" note and provides no actual code change (existing_code equals improved_code). Low impact.

Low
Possible issue
Snapshot samples to avoid race

Reading self.sampler.samples may not be thread-safe if the sampler's producer thread
is still appending. Previously, send_samples was called after
executor_future.result() returned, which implied producers had stopped. Now
send_samples is also invoked from periodic wakeups while samplers are still active.
Consider taking a snapshot (copy or swap) of samples to avoid concurrent mutation
while iterating downstream.

osbenchmark/worker_coordinator/worker_coordinator.py [2120-2128]

 def send_samples(self, joinpoint_reached=None):
-    if self.sampler:
-        samples = self.sampler.samples
-    else:
-        samples = []
-    if self.profile_sampler:
-        profile_samples = self.profile_sampler.samples
-    else:
-        profile_samples = []
+    samples = list(self.sampler.samples) if self.sampler else []
+    profile_samples = list(self.profile_sampler.samples) if self.profile_sampler else []
Suggestion importance[1-10]: 6

__

Why: Valid concern about thread-safety when send_samples is now invoked periodically while the sampler producer thread may still be appending. Taking a snapshot is a reasonable defensive measure.

Low
Fix always-true attribute check

hasattr(self, "sample_post_processor_actor") will always be True because the
attribute is initialized to None in init. Also, self.coordinator.close() sends a
CloseMetricsStore message to the same actor, so sending ActorExitRequest afterwards
can race with the close. Guard the shutdown with a None check and avoid the
duplicate close path.

osbenchmark/worker_coordinator/worker_coordinator.py [684-689]

 def receiveMsg_BenchmarkCancelled(self, msg, sender):
     self.logger.info("Main worker_coordinator received a notification that the benchmark has been cancelled.")
     self.coordinator.close()
-    if hasattr(self, "sample_post_processor_actor"):
+    if self.sample_post_processor_actor is not None:
         self.logger.info("Shutting down SamplePostProcessorActor due to benchmark cancellation.")
         self.send(self.sample_post_processor_actor, thespian.actors.ActorExitRequest())
Suggestion importance[1-10]: 5

__

Why: Correct observation that hasattr is always True since the attribute is initialized in __init__. The None check is more accurate, though the impact is minor since sending to None would just be a no-op error.

Low

Signed-off-by: Anthony Leong <aj.leong623@gmail.com>
@github-actions

Copy link
Copy Markdown

PR Code Analyzer ❗

AI-powered 'Code-Diff-Analyzer' found issues on commit 92c7eaf.

PathLineSeverityDescription
osbenchmark/benchmark.py36lowimport os added as a new import but is not used anywhere in the visible diff for this file. Unused imports of sensitive modules like os warrant review, though this is likely an oversight from copy-pasting the import block.
osbenchmark/benchmark.py35lowimport linecache added but not used in benchmark.py. linecache can read arbitrary files by path; its usage is justified in worker_coordinator.py for traceback display, but its presence here without usage is anomalous.
osbenchmark/worker_coordinator/worker_coordinator.py47lowimport os added as a new import but is not used in any of the visible diff changes for this file. No clear justification for adding it alongside the tracemalloc profiling code.
osbenchmark/worker_coordinator/worker_coordinator.py1558lowdisplay_top uses print() instead of the existing logger infrastructure. This bypasses log-level filtering and log redirection, causing memory profiling output to always appear on stdout regardless of logging configuration. Could obscure other output or indicate intent to ensure visibility outside normal logging channels.
osbenchmark/worker_coordinator/worker_coordinator.py1671lowlog_memory_usage called on every WakeupMessage in both WorkerCoordinatorActor and Worker actors. WakeupMessages are typically high-frequency in actor systems; calling tracemalloc.take_snapshot() on each wakeup introduces significant performance overhead and generates voluminous stdout output, which could mask other diagnostic output. This appears to be debug code not intended for production.

The table above displays the top 10 most important findings.

Total: 5 | Critical: 0 | High: 0 | Medium: 0 | Low: 5


Pull Requests Author(s): Please update your Pull Request according to the report above.

Repository Maintainer(s): You can bypass diff analyzer by adding label skip-diff-analyzer after reviewing the changes carefully, then re-run failed actions. To re-enable the analyzer, remove the label, then re-run all actions.


⚠️ Note: The Code-Diff-Analyzer helps protect against potentially harmful code patterns. Please ensure you have thoroughly reviewed the changes beforehand.

Thanks.

@github-actions

Copy link
Copy Markdown

Persistent review updated to latest commit 92c7eaf

Signed-off-by: Anthony Leong <aj.leong623@gmail.com>
@github-actions

Copy link
Copy Markdown

PR Code Analyzer ❗

AI-powered 'Code-Diff-Analyzer' found issues on commit b6de30c.

PathLineSeverityDescription
osbenchmark/worker_coordinator/worker_coordinator.py1600lowThe _write_memory_summary() function writes code_line snippets (captured via linecache) alongside allocation metadata to a local log file. While tracemalloc does not expose memory contents, source code lines written to logs could reveal implementation details if logs are stored in a shared or insufficiently protected location. This is a minor information disclosure risk with no evidence of malicious intent.

The table above displays the top 10 most important findings.

Total: 1 | Critical: 0 | High: 0 | Medium: 0 | Low: 1


Pull Requests Author(s): Please update your Pull Request according to the report above.

Repository Maintainer(s): You can bypass diff analyzer by adding label skip-diff-analyzer after reviewing the changes carefully, then re-run failed actions. To re-enable the analyzer, remove the label, then re-run all actions.


⚠️ Note: The Code-Diff-Analyzer helps protect against potentially harmful code patterns. Please ensure you have thoroughly reviewed the changes beforehand.

Thanks.

@github-actions

Copy link
Copy Markdown

Persistent review updated to latest commit b6de30c

Signed-off-by: Anthony Leong <aj.leong623@gmail.com>
@github-actions

Copy link
Copy Markdown

Persistent review updated to latest commit 88207f8

Signed-off-by: Anthony Leong <aj.leong623@gmail.com>
@github-actions

Copy link
Copy Markdown

PR Code Analyzer ❗

AI-powered 'Code-Diff-Analyzer' found issues on commit 46613ae.

PathLineSeverityDescription
osbenchmark/benchmark.py1324lowtracemalloc.start() is called unconditionally in main() without any configuration flag or guard. This adds memory profiling overhead to all benchmark runs, which is anomalous for a performance benchmarking tool, but no malicious intent is evident. Likely leftover debug instrumentation that was not gated behind a flag.
osbenchmark/worker_coordinator/worker_coordinator.py1610lowThe display_top/log_memory_usage functions capture source code lines (via linecache.getline) and write them to local log files as part of memory profiling. This is the standard Python tracemalloc usage pattern and writes only to the application's own log directory (paths.logs()). No external transmission is present. Flagged as an anomaly only because this profiling code appears partially commented-out, suggesting work-in-progress debug instrumentation inadvertently left active.

The table above displays the top 10 most important findings.

Total: 2 | Critical: 0 | High: 0 | Medium: 0 | Low: 2


Pull Requests Author(s): Please update your Pull Request according to the report above.

Repository Maintainer(s): You can bypass diff analyzer by adding label skip-diff-analyzer after reviewing the changes carefully, then re-run failed actions. To re-enable the analyzer, remove the label, then re-run all actions.


⚠️ Note: The Code-Diff-Analyzer helps protect against potentially harmful code patterns. Please ensure you have thoroughly reviewed the changes beforehand.

Thanks.

@github-actions

Copy link
Copy Markdown

Persistent review updated to latest commit 46613ae

Signed-off-by: Anthony Leong <aj.leong623@gmail.com>
@github-actions

Copy link
Copy Markdown

Persistent review updated to latest commit e255d1a

Signed-off-by: Anthony Leong <aj.leong623@gmail.com>
@github-actions

Copy link
Copy Markdown

PR Code Analyzer ❗

AI-powered 'Code-Diff-Analyzer' found issues on commit 1da9aaf.

PathLineSeverityDescription
osbenchmark/worker_coordinator/worker_coordinator.py691low_report_message_difference() is called on every WakeupMessage, appending to a log file on each invocation. This is an unusual pattern that could cause excessive disk I/O or disk exhaustion over long benchmark runs, but appears to be an incomplete/leftover debugging artifact rather than intentional malice.
osbenchmark/worker_coordinator/worker_coordinator.py925lowSampleUpdaterUnit actor class is defined but appears unused — it only relays UpdateSamples messages to self.parent with no other logic. The class has no parent reference set up and self.parent is never initialized. This seems like an incomplete refactoring artifact rather than a backdoor, but it is dead/unreachable code that is out of place.

The table above displays the top 10 most important findings.

Total: 2 | Critical: 0 | High: 0 | Medium: 0 | Low: 2


Pull Requests Author(s): Please update your Pull Request according to the report above.

Repository Maintainer(s): You can bypass diff analyzer by adding label skip-diff-analyzer after reviewing the changes carefully, then re-run failed actions. To re-enable the analyzer, remove the label, then re-run all actions.


⚠️ Note: The Code-Diff-Analyzer helps protect against potentially harmful code patterns. Please ensure you have thoroughly reviewed the changes beforehand.

Thanks.

@github-actions

Copy link
Copy Markdown

Persistent review updated to latest commit 1da9aaf

Signed-off-by: Anthony Leong <aj.leong623@gmail.com>
@github-actions

Copy link
Copy Markdown

Persistent review updated to latest commit 440f088

Signed-off-by: Anthony Leong <aj.leong623@gmail.com>
Signed-off-by: Anthony Leong <aj.leong623@gmail.com>
actor.coordinator.update_samples.assert_called_once_with(latest_progress_per_client)

def test_worker_coordinator_updates_latest_progress_per_client(self):
coordinator = worker_coordinator.WorkerCoordinator.__new__(worker_coordinator.WorkerCoordinator)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I would prefer if the tests followed the existing pattern for how coordinators are initialized.

Signed-off-by: Anthony Leong <aj.leong623@gmail.com>
Comment on lines +1442 to +1445
if msg.joinpoint_reached:
self.post_process_samples()
self.logger.debug("Join point reached message received in SamplePostProcessorActor. Notifying WorkerCoordinatorActor...")
self.send(self.worker_coordinator_actor, msg.joinpoint_reached)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

just to be safe can we wrap this in a try/finally just to make sure the benchmark never hangs and this actor always sends the joinpoint reached message?

self.target.send(self.target.sample_post_processor_actor,
GetExternalizableMetricsStore(True,
reason=ReasonForExternalizableRequest.TASK_FINISHED, waiting_period=waiting_period)
)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

There is a race condition here because the workers are being reactivated while the metric store results are in the process of being sent to the parent actor of worker_coordinator. This would mean that ProcessSample messages could be sent to the SPP actor while thematic store results are still being externalized.

…ore metric store is externalized.

Signed-off-by: Anthony Leong <aj.leong623@gmail.com>
Signed-off-by: Anthony Leong <aj.leong623@gmail.com>
@ajleong623

Copy link
Copy Markdown
Contributor Author

TaskBoundaryFlushed is sent from SPP actor to worker coordinator when the externalized metric store is ready to be submitted to the parent of the worker coordinator actor during the completion of a task.
FlushAndForwardJoinPoint sent from worker to SPP to post process the samples and also send the join point.
FlushForTaskBoundary sent from worker coordinator to SPP to tell SPP to post process samples and send an externalized metric store.
FlushAndClose sent from worker coordinator when the task is finished and a BenchmarkComplete needs to be sent with the metric store externalized.

The reason for these design choices are to further separate the different reasons for sending an externalized metric store and separating the join point forwarding logic instead of having it embedded within the ProcessSamples logic.

The new method drive_workers_for_next_task contains the separated logic from the original move_to_next_task method. This makes sure that the SPP actor has completed its post_processing and externalizing the metric store before the new workers are set when a join point is reached.

Additionally, try/catch blocks around post_process_samples method calls are added to make sure failures there do not stall the rest of the commands.

Let me know if these changes are sufficient. I still have to clean up the logging statements.

Comment on lines 687 to 692
if hasattr(self, "sample_post_processor_actor"):
self.logger.info("Shutting down SamplePostProcessorActor due to benchmark cancellation.")
self.send(self.sample_post_processor_actor, thespian.actors.ActorExitRequest())
# shut down FeedbackActor if it's active
# we do this manually in the workercoordinator since it's fully responsible for the feedback actor
if hasattr(self, "feedback_actor"):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

the two hasattr checks here are essentially no-ops since sample_post_processor_actor is set to None above. Easiest fix would be to say:

if hasattr(self, "sample_post_processor_actor") is not None:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Edit: my suggested fix would actually still be a no-op since hasattr(self, "sample_post_processor_actor") would return True and True is not None would be the same thing. Try this instead:

if self.sample_post_processor_actor is not None:

Comment on lines +767 to +769
@actor.no_retry("worker_coordinator") # pylint: disable=no-value-for-parameter
def receiveMsg_TaskFinished(self, msg, sender):
self.on_task_finished(msg.metrics, msg.next_task_scheduled_in)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

seems like this is now dead code and can be removed

Comment on lines +771 to +774
@actor.no_retry("worker_coordinator") # pylint: disable=no-value-for-parameter
def receiveMsg_TaskBoundaryFlushed(self, msg, sender):
self.on_task_finished(msg.metrics, msg.next_task_scheduled_in)
self.coordinator.drive_workers_for_next_task(msg.workers_curr_step, msg.next_task_scheduled_in)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think it's a good idea here to add a guard incase the benchmark is aborted

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

For this, I added a status variable to denote when the worker coordinator is exiting.

Comment on lines +1495 to +1497
if self.closed:
self.logger.debug("Ignoring task-boundary flush after SamplePostProcessorActor has closed.")
self.send(self.worker_coordinator_actor, TaskBoundaryFlushed(metric_results, msg.waiting_period, msg.workers_curr_step))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

if self.closed here let's just emit the debug log and not send anything

Comment on lines +1504 to +1508
except BaseException as e:
self.logger.exception("Could not flush samples at task boundary.")
self.send(self.worker_coordinator_actor, actor.BenchmarkFailure("Error in sample post processor ({})".format(str(e))))
finally:
self.send(self.worker_coordinator_actor, TaskBoundaryFlushed(metric_results, msg.waiting_period, msg.workers_curr_step))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think here in the event of an Exception we would send a failure but then proceed to send TaskBoundaryFlushed. This could lead to the workers being re-driven after declaring a failure

Comment on lines +1524 to +1529
except BaseException as e:
self.logger.exception("Could not flush and close samples at benchmark completion.")
self.send(self.worker_coordinator_actor, actor.BenchmarkFailure("Error in sample post processor ({})".format(str(e))))
self.close(process_samples=False, stop_telemetry=False, suppress_errors=True)
finally:
self.send(self.worker_coordinator_actor, BenchmarkComplete(metric_results))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same as my previous comment about double sending, here the orchestrator would see BenchmarkFailure but then BenchmarkComplete which I think could lead to confusion.

Let's only send TaskBoundaryFlushed or BenchmarkComplete messages on the success path

@OVI3D0

OVI3D0 commented Jun 30, 2026

Copy link
Copy Markdown
Member

once you rebase the unit tests failures about stream_writer should stop, but looks like there is one too many public methods error also

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

Persistent review updated to latest commit ab2e077

Signed-off-by: Anthony Leong <aj.leong623@gmail.com>
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

Persistent review updated to latest commit 13c52a3

Signed-off-by: Anthony Leong <aj.leong623@gmail.com>
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

Persistent review updated to latest commit 27d93db

Signed-off-by: Anthony Leong <aj.leong623@gmail.com>
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

Persistent review updated to latest commit 33c6735

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown

Persistent review updated to latest commit d380153

@OVI3D0 OVI3D0 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This LGTM and will be included with OSB's 3.0 release.

Signed-off-by: Anthony Leong <aj.leong623@gmail.com>
@github-actions

Copy link
Copy Markdown

Persistent review updated to latest commit a3bf0c9

@ajleong623

Copy link
Copy Markdown
Contributor Author

I think the last merge conflict came from registering the new telemetry device in #1102. I resolved it by making sure that the new device is registered in the updated prepare_telemetry location (That method was moved by this change).

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

Labels

None yet

Projects

Status: 👀 In Review

Development

Successfully merging this pull request may close these issues.

2 participants