Skip to content

Commit 3a78aff

Browse files
author
Alex Wang
committed
fix(insight): preserve bounded renderer values
1 parent 4bd5e14 commit 3a78aff

2 files changed

Lines changed: 88 additions & 11 deletions

File tree

packages/aws-durable-execution-sdk-python-insight/src/aws_durable_execution_sdk_python_insight/_export_scheduler.py

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,16 @@
2121
from __future__ import annotations
2222

2323
import copy
24+
import datetime
25+
import decimal
2426
import functools
2527
import itertools
2628
import logging
2729
import sys
2830
import threading
2931
import time
3032
import types
33+
import uuid
3134
from collections import OrderedDict, deque
3235
from typing import Any
3336

@@ -69,6 +72,16 @@
6972
)
7073

7174

75+
_SAFE_OPAQUE_RETAINED_TYPES = (
76+
datetime.date,
77+
datetime.datetime,
78+
datetime.time,
79+
datetime.timedelta,
80+
decimal.Decimal,
81+
uuid.UUID,
82+
)
83+
84+
7285
class _RetainedChildren:
7386
__slots__ = ("iterator",)
7487

@@ -113,29 +126,39 @@ def _retained_children(value: Any) -> Any:
113126
if isinstance(value, deque):
114127
return itertools.chain(deque.__iter__(value), custom)
115128
if isinstance(value, memoryview):
116-
return (value.obj,)
129+
return itertools.chain((value.obj,), custom)
117130
if isinstance(value, functools.partial):
118-
return (value.func, value.args, value.keywords)
131+
return itertools.chain((value.func, value.args, value.keywords), custom)
119132
if isinstance(value, types.FunctionType):
120133
closure = []
121134
for cell in value.__closure__ or ():
122135
try:
123136
closure.append(cell.cell_contents)
124137
except ValueError:
125138
pass
126-
return itertools.chain(closure, (value.__defaults__, value.__kwdefaults__))
139+
return itertools.chain(
140+
closure, (value.__defaults__, value.__kwdefaults__), custom
141+
)
127142
if isinstance(value, types.MethodType):
128-
return (value.__self__, value.__func__)
143+
return itertools.chain((value.__self__, value.__func__), custom)
129144
if isinstance(value, types.BuiltinFunctionType):
130145
owner = value.__self__
131-
return () if owner is None or isinstance(owner, types.ModuleType) else (owner,)
146+
retained = (
147+
() if owner is None or isinstance(owner, types.ModuleType) else (owner,)
148+
)
149+
return itertools.chain(retained, custom)
132150
if isinstance(value, types.MethodWrapperType):
133-
return (value.__self__,)
151+
return itertools.chain((value.__self__,), custom)
134152
if isinstance(value, types.GeneratorType):
135153
frame = value.gi_frame
136-
return () if frame is None else (frame.f_locals, value.gi_yieldfrom)
137-
if isinstance(value, _ATOMIC_RETAINED_TYPES):
138-
return ()
154+
generator_children = (
155+
() if frame is None else (frame.f_locals, value.gi_yieldfrom)
156+
)
157+
return itertools.chain(generator_children, custom)
158+
if type(value) in _ATOMIC_RETAINED_TYPES:
159+
return custom
160+
if type(value) in _SAFE_OPAQUE_RETAINED_TYPES:
161+
return custom
139162
if custom:
140163
return custom
141164
return _UNSUPPORTED_RETAINED_GRAPH
@@ -568,7 +591,16 @@ def __init__(
568591

569592
def schedule(self, execution_arn: str, record: dict[str, Any]) -> None:
570593
"""Fan a canonical record out to every lane. Returns immediately."""
571-
record_size = _estimate_retained_size(record, self._max_pending_bytes)
594+
try:
595+
record_size = _estimate_retained_size(record, self._max_pending_bytes)
596+
except Exception as exc: # noqa: BLE001 - inspection must never break a hook
597+
_logger.warning(
598+
"workflow-insight: retained-size inspection failed for %s; "
599+
"rejecting this record safely: %s",
600+
execution_arn,
601+
exc,
602+
)
603+
record_size = self._max_pending_bytes + 1
572604
for lane in self._lanes:
573605
lane.schedule(execution_arn, record, record_size)
574606

packages/aws-durable-execution-sdk-python-insight/tests/test_export_scheduler.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,15 @@
1111

1212
from __future__ import annotations
1313

14+
import datetime
1415
import functools
1516
import threading
1617
import time
1718
import types
1819
from typing import Any
1920

21+
import aws_durable_execution_sdk_python_insight._export_scheduler as scheduler_module
22+
2023
from aws_durable_execution_sdk_python_insight._export_scheduler import (
2124
_ExportScheduler,
2225
)
@@ -721,12 +724,18 @@ def __iter__(self):
721724
self.iterated = True
722725
return iter(())
723726

724-
backing_buffers = [bytearray(4_000) for _ in range(3)]
727+
class PayloadPartial(functools.partial):
728+
pass
729+
730+
backing_buffers = [bytearray(4_000) for _ in range(4)]
725731
hidden = HiddenList(backing_buffers[2])
732+
partial_with_payload = PayloadPartial(lambda value: value, "small")
733+
partial_with_payload.payload = backing_buffers[3]
726734
payloads = [
727735
functools.partial(lambda value: value, backing_buffers[0]),
728736
(value for value in (backing_buffers[1],)),
729737
hidden,
738+
partial_with_payload,
730739
]
731740

732741
for payload in payloads:
@@ -775,6 +784,42 @@ def closure() -> bytearray:
775784
scheduler.end_invocation(5.0)
776785

777786

787+
def test_safe_opaque_datetime_reaches_custom_renderer():
788+
class DateRenderExporter(RecordingExporter):
789+
def __init__(self) -> None:
790+
super().__init__(max_record_size_bytes=10_000)
791+
self.rendered: list[str] = []
792+
793+
def render(self, record: dict[str, Any]) -> Any:
794+
value = record["payload"].isoformat()
795+
self.rendered.append(value)
796+
return {"value": value}
797+
798+
exporter = DateRenderExporter()
799+
scheduler = _ExportScheduler([exporter])
800+
record = _rec(ARN_A, "date")
801+
record["payload"] = datetime.date(2026, 9, 10)
802+
scheduler.schedule(ARN_A, record)
803+
scheduler.end_invocation(5.0)
804+
805+
assert exporter.rendered == ["2026-09-10"]
806+
assert exporter.exported_values() == ["date"]
807+
808+
809+
def test_retained_size_inspection_failure_does_not_escape_schedule(monkeypatch):
810+
def fail_estimate(value: Any, max_size: int | None = None) -> int:
811+
raise RuntimeError("inspection failed")
812+
813+
monkeypatch.setattr(scheduler_module, "_estimate_retained_size", fail_estimate)
814+
exporter = RecordingExporter()
815+
scheduler = _ExportScheduler([exporter], max_pending_bytes=2_500)
816+
817+
scheduler.schedule(ARN_A, _rec(ARN_A, "rejected"))
818+
819+
assert scheduler._lanes[0]._pending_count() == 0
820+
assert scheduler._lanes[0]._worker is None
821+
822+
778823
def test_timed_out_barrier_flushes_eventually_and_worker_exits():
779824
exporter = BlockingExporter()
780825
scheduler = _ExportScheduler([exporter])

0 commit comments

Comments
 (0)