fix(core): prevent RuntimeError in Serializable.to_json() under thread concurrency#35340
Open
Balaji Seshadri (gitbalaji) wants to merge 3 commits intolangchain-ai:masterfrom
Open
Conversation
Merging this PR will improve performance by 34.02%
|
| Mode | Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|---|
| ⚡ | WallTime | test_import_time[InMemoryRateLimiter] |
184 ms | 161.6 ms | +13.91% |
| ⚡ | WallTime | test_import_time[Document] |
201 ms | 168.7 ms | +19.12% |
| ⚡ | WallTime | test_import_time[CallbackManager] |
344.5 ms | 292.4 ms | +17.81% |
| ⚡ | WallTime | test_import_time[ChatPromptTemplate] |
680.4 ms | 557.9 ms | +21.98% |
| ⚡ | WallTime | test_import_time[InMemoryVectorStore] |
659.2 ms | 530.1 ms | +24.36% |
| ⚡ | WallTime | test_import_time[LangChainTracer] |
487.6 ms | 416.8 ms | +16.98% |
| ⚡ | WallTime | test_import_time[BaseChatModel] |
580.6 ms | 484.8 ms | +19.76% |
| ⚡ | WallTime | test_async_callbacks_in_sync |
24.5 ms | 18.3 ms | +34.02% |
| ⚡ | WallTime | test_import_time[Runnable] |
533.9 ms | 443.3 ms | +20.45% |
| ⚡ | WallTime | test_import_time[RunnableLambda] |
540.3 ms | 446.2 ms | +21.09% |
| ⚡ | WallTime | test_import_time[HumanMessage] |
287.6 ms | 239.6 ms | +20.04% |
| ⚡ | WallTime | test_import_time[tool] |
601.5 ms | 478.3 ms | +25.76% |
| ⚡ | WallTime | test_import_time[PydanticOutputParser] |
583.2 ms | 478.4 ms | +21.91% |
Comparing gitbalaji:fix/core-serializable-thread-safe (5134cb4) with master (d6e46bb)
Footnotes
-
23 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩
5134cb4 to
2ec5887
Compare
…rrency Fixes langchain-ai#34887. Pydantic's `__iter__` walks `self.__dict__.items()` live. A concurrent thread that triggers a `@cached_property` for the first time (e.g. `_serialized` on `BasePromptTemplate`) writes a new key into `__dict__`, racing with the loop and raising: RuntimeError: dictionary changed size during iteration Fix: snapshot the iteration with `list(self)` before the loop so any concurrent write is safe. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous attempt (`list(self)`) was insufficient: Pydantic's `__iter__` still walks `self.__dict__.items()` internally, so the RuntimeError fires inside `list()` when a concurrent @cached_property write races with that inner iteration. Iterate `type(self).model_fields` (a class-level dict, immutable at runtime) instead, fetching each value with `getattr`. This avoids touching `self.__dict__` as an iterator target entirely. Fixes langchain-ai#34887. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Simpler fix for langchain-ai#34887. dict.copy() is atomic under CPython's GIL, so it produces a consistent snapshot that cannot race with a concurrent @cached_property write. Keeps the original iteration logic intact. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2ec5887 to
31b7c3a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Serializable.to_json()iterated overself(which calls Pydantic__iter__, i.e.self.__dict__.items()live). A concurrent thread that accesses a@cached_propertyfor the first time (e.g._serializedonBasePromptTemplate.invoke()) writes a new key intoself.__dict__, causingRuntimeError: dictionary changed size during iteration.__dict__before iterating —self.__dict__.copy().items().dict.copy()is atomic under CPython's GIL, so it produces a consistent snapshot that cannot race with a concurrent@cached_propertywrite. Keeps the original iteration logic intact with minimal diff.test_to_json_thread_safe— spawns 4 threads running concurrentdumpd()and.invoke()calls against the same prompt instance. Reliably reproduced the crash without the fix.Fixes #34887.
Areas requiring careful review
self.__dict__.copy()is a shallow copy taken atomically under CPython's GIL. Once the copy exists, we iterate over a separate dict object, so any concurrent@cached_propertywrite to the original__dict__is harmless.__dict__(e.g. cached properties like_serialized) are already filtered out by_is_field_useful, so the finallc_kwargsis identical to before.Test plan
test_to_json_thread_safepasses and would fail on the unfixed codetests/unit_tests/load/test_serializable.pysuite passes (44 tests)ruff checkandruff formatcleanmypyclean🤖 Generated with Claude Code