diff --git a/packages/aws-durable-execution-sdk-python-insight/README.md b/packages/aws-durable-execution-sdk-python-insight/README.md index 44196691..ed47c053 100644 --- a/packages/aws-durable-execution-sdk-python-insight/README.md +++ b/packages/aws-durable-execution-sdk-python-insight/README.md @@ -55,6 +55,11 @@ and `top-level` vs `full-tree` operation detail all mirror the JS plugin. Behavior is validated cross-SDK by the `insight` conformance suite (`aws-durable-execution-conformance-tests-insight`). +> **Note (`on-change` emission).** In `on-change` mode, exporter calls currently +> run synchronously on the SDK checkpoint path, so a slow exporter can delay +> workflow progress. Asynchronous scheduling/coalescing is deferred and tracked +> in [issue #687](https://github.com/aws/aws-durable-execution-sdk-python/issues/687). + ## Requirements - `aws-durable-execution-sdk-python` with the plugin invocation hooks that diff --git a/packages/aws-durable-execution-sdk-python-insight/src/aws_durable_execution_sdk_python_insight/plugin.py b/packages/aws-durable-execution-sdk-python-insight/src/aws_durable_execution_sdk_python_insight/plugin.py index d5d23be2..13f16a06 100644 --- a/packages/aws-durable-execution-sdk-python-insight/src/aws_durable_execution_sdk_python_insight/plugin.py +++ b/packages/aws-durable-execution-sdk-python-insight/src/aws_durable_execution_sdk_python_insight/plugin.py @@ -32,6 +32,7 @@ import datetime import json +import math import sys import threading from typing import Any, Callable @@ -107,6 +108,13 @@ def _resolve_sampling_rate(rate: float | None) -> float: return 1.0 if not isinstance(rate, (int, float)): return 1.0 + if isinstance(rate, float) and math.isnan(rate): + # Fail open. NaN compares False to everything, so without this guard it + # would flow through _should_sample (rate >= 1 -> False, rate <= 0 -> + # False, x < NaN -> False) and silently sample OUT every execution, + # disabling all instrumentation. Coerce to full sampling instead, which + # matches the JS plugin's treatment of non-finite/invalid rates. + return 1.0 if rate < 0 or rate > 1: return max(0.0, min(1.0, float(rate))) return float(rate) diff --git a/packages/aws-durable-execution-sdk-python-insight/tests/test_plugin.py b/packages/aws-durable-execution-sdk-python-insight/tests/test_plugin.py index 087ed0ca..485bcaab 100644 --- a/packages/aws-durable-execution-sdk-python-insight/tests/test_plugin.py +++ b/packages/aws-durable-execution-sdk-python-insight/tests/test_plugin.py @@ -38,6 +38,7 @@ WorkflowInsightConfig, workflow_insight, ) +from aws_durable_execution_sdk_python_insight.plugin import _resolve_sampling_rate ARN = "arn:aws:lambda:us-west-2:123456789012:function:my-fn:$LATEST/durable-execution/exec-1/inv-1" ARN_B = "arn:aws:lambda:us-west-2:123456789012:function:my-fn:$LATEST/durable-execution/exec-2/inv-1" @@ -196,6 +197,22 @@ def test_sampling_zero_emits_nothing(): assert exporter.records == [] +def test_resolve_sampling_rate_nan_fails_open_to_one(): + # NaN compares False to everything; without the guard this would sample OUT + # every execution. It must fail open to full sampling (JS parity). + assert _resolve_sampling_rate(float("nan")) == 1.0 + + +def test_nan_sampling_rate_emits_instead_of_silently_disabling(): + exporter = CaptureExporter() + plugin = workflow_insight( + WorkflowInsightConfig(exporters=[exporter], sampling_rate=float("nan")) + ) + _run(plugin, ops=[_step("greet")]) + # A NaN rate must not disable instrumentation: the record is still emitted. + assert len(exporter.records) == 1 + + def test_content_omit_input_output_without_drop_flags(): exporter = CaptureExporter() plugin = workflow_insight(