Skip to content

Commit f44b49b

Browse files
chore: dedup warnings (#7257)
Co-authored-by: Will Fu-Hinthorn <will@langchain.dev>
1 parent a0a95df commit f44b49b

4 files changed

Lines changed: 61 additions & 5 deletions

File tree

libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,23 @@
4646
EMPTY_BYTES = b""
4747
logger = logging.getLogger(__name__)
4848

49+
# Dedup log warnings across process lifetime; cap bounds state if types are
50+
# dynamically generated (also acts as a circuit breaker on warning volume).
51+
# Dedup is best-effort: racing threads may each emit once for the same key,
52+
# and warnings are silently dropped once _MAX_WARNED_TYPES is reached.
53+
_MAX_WARNED_TYPES = 1000
54+
_warned_unregistered_types: set[tuple[str, str]] = set()
55+
_warned_blocked_types: set[tuple[str, str]] = set()
56+
57+
58+
def _warn_once(
59+
seen: set[tuple[str, str]], key: tuple[str, str], msg: str, *args: object
60+
) -> None:
61+
if key in seen or len(seen) >= _MAX_WARNED_TYPES:
62+
return
63+
seen.add(key)
64+
logger.warning(msg, *args)
65+
4966

5067
class JsonPlusSerializer(SerializerProtocol):
5168
"""Serializer that uses ormsgpack, with optional fallbacks.
@@ -534,7 +551,9 @@ def _check_allowed(module: str, name: str) -> bool:
534551
"name": name,
535552
}
536553
)
537-
logger.warning(
554+
_warn_once(
555+
_warned_unregistered_types,
556+
key,
538557
"Deserializing unregistered type %s.%s from checkpoint. "
539558
"This will be blocked in a future version. "
540559
"Set LANGGRAPH_STRICT_MSGPACK=true to block now, or add "
@@ -556,7 +575,9 @@ def _check_allowed(module: str, name: str) -> bool:
556575
"name": name,
557576
}
558577
)
559-
logger.warning(
578+
_warn_once(
579+
_warned_blocked_types,
580+
key,
560581
"Blocked deserialization of %s.%s - not in allowed_msgpack_modules. "
561582
"Add to allowed_msgpack_modules to allow: [(%r, %r)]",
562583
module,

libs/checkpoint/tests/test_encrypted.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
EXT_METHOD_SINGLE_ARG,
3030
JsonPlusSerializer,
3131
_msgpack_enc,
32+
_warned_blocked_types,
33+
_warned_unregistered_types,
3234
)
3335

3436

@@ -102,6 +104,13 @@ def test_msgpack_method_pathlib_blocked_encrypted_strict(
102104
class TestEncryptedSerializerMsgpackAllowlist:
103105
"""Test msgpack allowlist behavior through EncryptedSerializer."""
104106

107+
@pytest.fixture(autouse=True)
108+
def _reset_warned_types(self) -> None:
109+
# Warning dedup state is process-global; reset per-test so each case
110+
# sees a fresh slate and assertions about warning emission are stable.
111+
_warned_unregistered_types.clear()
112+
_warned_blocked_types.clear()
113+
105114
def test_safe_types_no_warning(self, caplog: pytest.LogCaptureFixture) -> None:
106115
"""Test safe types deserialize without warnings through encryption."""
107116
serde = _make_encrypted_serde()

libs/checkpoint/tests/test_jsonplus.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
JsonPlusSerializer,
3636
_msgpack_enc,
3737
_msgpack_ext_hook_to_json,
38+
_warned_blocked_types,
39+
_warned_unregistered_types,
3840
)
3941
from langgraph.store.base import Item
4042

@@ -580,6 +582,14 @@ def test_msgpack_safe_types_no_warning(caplog: pytest.LogCaptureFixture) -> None
580582
assert result is not None
581583

582584

585+
@pytest.fixture(autouse=True)
586+
def _reset_warned_types() -> None:
587+
# Warning dedup state is process-global; reset per-test so each case sees
588+
# a fresh slate and assertions about warning emission are stable.
589+
_warned_unregistered_types.clear()
590+
_warned_blocked_types.clear()
591+
592+
583593
def test_msgpack_pydantic_warns_by_default(caplog: pytest.LogCaptureFixture) -> None:
584594
"""Pydantic models not in allowlist should log warning but still deserialize."""
585595
current = _lg_msgpack.STRICT_MSGPACK_ENABLED
@@ -595,6 +605,12 @@ def test_msgpack_pydantic_warns_by_default(caplog: pytest.LogCaptureFixture) ->
595605
assert "unregistered type" in caplog.text.lower()
596606
assert "allowed_msgpack_modules" in caplog.text
597607
assert result == obj
608+
609+
# Second deserialization of the same type should NOT produce another warning
610+
caplog.clear()
611+
result2 = serde.loads_typed(dumped)
612+
assert "unregistered type" not in caplog.text.lower()
613+
assert result2 == obj
598614
_lg_msgpack.STRICT_MSGPACK_ENABLED = current
599615

600616

@@ -639,7 +655,6 @@ def test_msgpack_allowlist_silences_warning(caplog: pytest.LogCaptureFixture) ->
639655

640656
def test_msgpack_none_blocks_unregistered(caplog: pytest.LogCaptureFixture) -> None:
641657
"""allowed_msgpack_modules=None should block unregistered types."""
642-
643658
serde = JsonPlusSerializer(allowed_msgpack_modules=None)
644659

645660
obj = MyPydantic(foo="test", bar=42, inner=InnerPydantic(hello="world"))
@@ -657,7 +672,6 @@ def test_msgpack_allowlist_blocks_non_listed(
657672
caplog: pytest.LogCaptureFixture,
658673
) -> None:
659674
"""Allowlists should block unregistered types even if msgpack is enabled."""
660-
661675
serde = JsonPlusSerializer(
662676
allowed_msgpack_modules=[("tests.test_jsonplus", "MyPydantic")]
663677
)

libs/checkpoint/tests/test_memory.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,25 @@
1212
empty_checkpoint,
1313
)
1414
from langgraph.checkpoint.memory import InMemorySaver
15-
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
15+
from langgraph.checkpoint.serde.jsonplus import (
16+
JsonPlusSerializer,
17+
_warned_blocked_types,
18+
_warned_unregistered_types,
19+
)
1620

1721

1822
class MemoryPydantic(BaseModel):
1923
foo: str
2024

2125

26+
@pytest.fixture(autouse=True)
27+
def _reset_warned_types() -> None:
28+
# Warning dedup state is process-global; reset per-test so each case sees
29+
# a fresh slate and assertions about warning emission are stable.
30+
_warned_unregistered_types.clear()
31+
_warned_blocked_types.clear()
32+
33+
2234
class TestMemorySaver:
2335
@pytest.fixture(autouse=True)
2436
def setup(self) -> None:

0 commit comments

Comments
 (0)