Skip to content

Commit 5558c18

Browse files
author
Alex Wang
committed
fix(insight): preserve bounded FIFO renderer values
1 parent 53ef3be commit 5558c18

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
@@ -20,13 +20,16 @@
2020
from __future__ import annotations
2121

2222
import copy
23+
import datetime
24+
import decimal
2325
import functools
2426
import itertools
2527
import logging
2628
import sys
2729
import threading
2830
import time
2931
import types
32+
import uuid
3033
from collections import deque
3134
from dataclasses import dataclass
3235
from typing import Any
@@ -79,6 +82,16 @@
7982
)
8083

8184

85+
_SAFE_OPAQUE_RETAINED_TYPES = (
86+
datetime.date,
87+
datetime.datetime,
88+
datetime.time,
89+
datetime.timedelta,
90+
decimal.Decimal,
91+
uuid.UUID,
92+
)
93+
94+
8295
class _RetainedChildren:
8396
__slots__ = ("iterator",)
8497

@@ -123,29 +136,39 @@ def _retained_children(value: Any) -> Any:
123136
if isinstance(value, deque):
124137
return itertools.chain(deque.__iter__(value), custom)
125138
if isinstance(value, memoryview):
126-
return (value.obj,)
139+
return itertools.chain((value.obj,), custom)
127140
if isinstance(value, functools.partial):
128-
return (value.func, value.args, value.keywords)
141+
return itertools.chain((value.func, value.args, value.keywords), custom)
129142
if isinstance(value, types.FunctionType):
130143
closure = []
131144
for cell in value.__closure__ or ():
132145
try:
133146
closure.append(cell.cell_contents)
134147
except ValueError:
135148
pass
136-
return itertools.chain(closure, (value.__defaults__, value.__kwdefaults__))
149+
return itertools.chain(
150+
closure, (value.__defaults__, value.__kwdefaults__), custom
151+
)
137152
if isinstance(value, types.MethodType):
138-
return (value.__self__, value.__func__)
153+
return itertools.chain((value.__self__, value.__func__), custom)
139154
if isinstance(value, types.BuiltinFunctionType):
140155
owner = value.__self__
141-
return () if owner is None or isinstance(owner, types.ModuleType) else (owner,)
156+
retained = (
157+
() if owner is None or isinstance(owner, types.ModuleType) else (owner,)
158+
)
159+
return itertools.chain(retained, custom)
142160
if isinstance(value, types.MethodWrapperType):
143-
return (value.__self__,)
161+
return itertools.chain((value.__self__,), custom)
144162
if isinstance(value, types.GeneratorType):
145163
frame = value.gi_frame
146-
return () if frame is None else (frame.f_locals, value.gi_yieldfrom)
147-
if isinstance(value, _ATOMIC_RETAINED_TYPES):
148-
return ()
164+
generator_children = (
165+
() if frame is None else (frame.f_locals, value.gi_yieldfrom)
166+
)
167+
return itertools.chain(generator_children, custom)
168+
if type(value) in _ATOMIC_RETAINED_TYPES:
169+
return custom
170+
if type(value) in _SAFE_OPAQUE_RETAINED_TYPES:
171+
return custom
149172
if custom:
150173
return custom
151174
return _UNSUPPORTED_RETAINED_GRAPH
@@ -672,7 +695,16 @@ def __init__(
672695

673696
def schedule(self, execution_arn: str, record: dict[str, Any]) -> None:
674697
"""Fan a canonical record out to every lane. Returns immediately."""
675-
record_size = _estimate_retained_size(record, self._max_pending_bytes)
698+
try:
699+
record_size = _estimate_retained_size(record, self._max_pending_bytes)
700+
except Exception as exc: # noqa: BLE001 - inspection must never break a hook
701+
_logger.warning(
702+
"workflow-insight: retained-size inspection failed for %s; "
703+
"rejecting this record safely: %s",
704+
execution_arn,
705+
exc,
706+
)
707+
record_size = self._max_pending_bytes + 1
676708
for lane in self._lanes:
677709
lane.schedule(execution_arn, record, record_size)
678710

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
)
@@ -900,12 +903,18 @@ def __iter__(self):
900903
self.iterated = True
901904
return iter(())
902905

903-
backing_buffers = [bytearray(4_000) for _ in range(3)]
906+
class PayloadPartial(functools.partial):
907+
pass
908+
909+
backing_buffers = [bytearray(4_000) for _ in range(4)]
904910
hidden = HiddenList(backing_buffers[2])
911+
partial_with_payload = PayloadPartial(lambda value: value, "small")
912+
partial_with_payload.payload = backing_buffers[3]
905913
payloads = [
906914
functools.partial(lambda value: value, backing_buffers[0]),
907915
(value for value in (backing_buffers[1],)),
908916
hidden,
917+
partial_with_payload,
909918
]
910919

911920
for payload in payloads:
@@ -954,6 +963,42 @@ def closure() -> bytearray:
954963
scheduler.end_invocation(5.0)
955964

956965

966+
def test_safe_opaque_datetime_reaches_custom_renderer():
967+
class DateRenderExporter(RecordingExporter):
968+
def __init__(self) -> None:
969+
super().__init__(max_record_size_bytes=10_000)
970+
self.rendered: list[str] = []
971+
972+
def render(self, record: dict[str, Any]) -> Any:
973+
value = record["payload"].isoformat()
974+
self.rendered.append(value)
975+
return {"value": value}
976+
977+
exporter = DateRenderExporter()
978+
scheduler = _ExportScheduler([exporter])
979+
record = _rec(ARN_A, "date")
980+
record["payload"] = datetime.date(2026, 9, 10)
981+
scheduler.schedule(ARN_A, record)
982+
scheduler.end_invocation(5.0)
983+
984+
assert exporter.rendered == ["2026-09-10"]
985+
assert exporter.exported_values() == ["date"]
986+
987+
988+
def test_retained_size_inspection_failure_does_not_escape_schedule(monkeypatch):
989+
def fail_estimate(value: Any, max_size: int | None = None) -> int:
990+
raise RuntimeError("inspection failed")
991+
992+
monkeypatch.setattr(scheduler_module, "_estimate_retained_size", fail_estimate)
993+
exporter = RecordingExporter()
994+
scheduler = _ExportScheduler([exporter], max_pending_bytes=2_500)
995+
996+
scheduler.schedule(ARN_A, _rec(ARN_A, "rejected"))
997+
998+
assert scheduler._lanes[0]._pending_count() == 0
999+
assert scheduler._lanes[0]._worker is None
1000+
1001+
9571002
def test_timed_out_barrier_flushes_eventually_and_worker_exits():
9581003
exporter = BlockingExporter()
9591004
scheduler = _ExportScheduler([exporter])

0 commit comments

Comments
 (0)