Skip to content

Commit 04d7e1e

Browse files
authored
Fix WeakMethod Crash in LangGraph (#1755)
* Fix crash in WeakMethod * Update tests
1 parent 67cce6f commit 04d7e1e

2 files changed

Lines changed: 61 additions & 30 deletions

File tree

newrelic/hooks/mlmodel_langgraph.py

Lines changed: 43 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import functools
16+
1517
from newrelic.api.time_trace import current_trace
1618
from newrelic.api.transaction import current_transaction
1719
from newrelic.common.async_wrapper import coroutine_wrapper
18-
from newrelic.common.object_wrapper import wrap_function_wrapper
20+
from newrelic.common.object_wrapper import wrap_function_wrapper, wrap_object
1921
from newrelic.common.signature import bind_args
2022
from newrelic.core.context import ContextOf, context_wrapper
2123
from newrelic.hooks.mlmodel_langchain import wrap_run_in_executor
@@ -57,33 +59,49 @@ def bind_submit(func, *args, **kwargs):
5759
return func, args, kwargs
5860

5961

60-
def wrap_BackgroundExecutor_submit(wrapped, instance, args, kwargs):
61-
trace = current_trace()
62-
if not trace:
63-
return wrapped(*args, **kwargs)
62+
def wrap_BackgroundExecutor_submit(wrapped):
63+
# We can't use wrapt FuctionWrapper here because the function will be wrapped in a weakref.WeakMethod,
64+
# which isn't compatible with wrapt. Instead, we have to do the wrapping manually. We use functools.wraps
65+
# to preserve the original function's signature, name, and annotations as best as we can.
66+
@functools.wraps(wrapped)
67+
def _wrapper(self, *args, **kwargs):
68+
# This will be a bound method, so the first argument must always be self.
69+
trace = current_trace()
70+
if not trace:
71+
return wrapped(self, *args, **kwargs)
6472

65-
try:
66-
func, args, kwargs = bind_submit(*args, **kwargs)
67-
except Exception:
68-
return wrapped(*args, **kwargs)
73+
try:
74+
func, args, kwargs = bind_submit(*args, **kwargs)
75+
except Exception:
76+
return wrapped(self, *args, **kwargs)
6977

70-
func = context_wrapper(func, trace=trace, strict=True)
71-
return wrapped(func, *args, **kwargs)
78+
func = context_wrapper(func, trace=trace, strict=True)
79+
return wrapped(self, func, *args, **kwargs)
7280

81+
return _wrapper
7382

74-
def wrap_AsyncBackgroundExecutor_submit(wrapped, instance, args, kwargs):
75-
trace = current_trace()
76-
if not trace:
77-
return wrapped(*args, **kwargs)
7883

79-
try:
80-
func, args, kwargs = bind_submit(*args, **kwargs)
81-
except Exception:
82-
return wrapped(*args, **kwargs)
84+
def wrap_AsyncBackgroundExecutor_submit(wrapped):
85+
# We can't use wrapt FuctionWrapper here because the function will be wrapped in a weakref.WeakMethod,
86+
# which isn't compatible with wrapt. Instead, we have to do the wrapping manually. We use functools.wraps
87+
# to preserve the original function's signature, name, and annotations as best as we can.
88+
@functools.wraps(wrapped)
89+
def _wrapper(self, *args, **kwargs):
90+
# This will be a bound method, so the first argument must always be self.
91+
trace = current_trace()
92+
if not trace:
93+
return wrapped(self, *args, **kwargs)
8394

84-
context = ContextOf(trace=trace, strict=True)
85-
func = coroutine_wrapper(func, context)
86-
return wrapped(func, *args, **kwargs)
95+
try:
96+
func, args, kwargs = bind_submit(*args, **kwargs)
97+
except Exception:
98+
return wrapped(self, *args, **kwargs)
99+
100+
context = ContextOf(trace=trace, strict=True)
101+
func = coroutine_wrapper(func, context)
102+
return wrapped(self, func, *args, **kwargs)
103+
104+
return _wrapper
87105

88106

89107
def instrument_langgraph_prebuilt_tool_node(module):
@@ -96,9 +114,10 @@ def instrument_langgraph_prebuilt_tool_node(module):
96114

97115
def instrument_langgraph_pregel_executor(module):
98116
if hasattr(module, "BackgroundExecutor"):
99-
wrap_function_wrapper(module, "BackgroundExecutor.submit", wrap_BackgroundExecutor_submit)
117+
wrap_object(module, "BackgroundExecutor.submit", wrap_BackgroundExecutor_submit)
118+
100119
if hasattr(module, "AsyncBackgroundExecutor"):
101-
wrap_function_wrapper(module, "AsyncBackgroundExecutor.submit", wrap_AsyncBackgroundExecutor_submit)
120+
wrap_object(module, "AsyncBackgroundExecutor.submit", wrap_AsyncBackgroundExecutor_submit)
102121

103122

104123
def instrument_langgraph_internal_runnable(module):

tests/mlmodel_langchain/test_pregel_executor.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,42 +13,54 @@
1313
# limitations under the License.
1414

1515
import asyncio
16+
import weakref
1617
from threading import Event
1718

19+
import pytest
1820
from langgraph.pregel._executor import AsyncBackgroundExecutor, BackgroundExecutor
1921

2022
from newrelic.api.background_task import background_task
2123
from newrelic.api.time_trace import current_trace
2224

2325

26+
@pytest.mark.parametrize("as_weakref", [False, True], ids=["strongref", "weakref"])
2427
@background_task()
25-
def test_background_executor_submit_propagates_context():
28+
def test_background_executor_submit_propagates_context(as_weakref):
2629
trace = current_trace()
2730
test_ran = Event()
2831

29-
def task():
32+
def task(arg1):
3033
assert current_trace() is trace
34+
assert arg1 == "test"
3135
test_ran.set()
3236

3337
with BackgroundExecutor(config={}) as submit:
34-
future = submit(task)
38+
if as_weakref:
39+
# Derefence the weakmethod to ensure we don't cause a crash
40+
submit = weakref.WeakMethod(submit)()
41+
future = submit(task, "test")
3542
future.result()
3643

3744
assert test_ran.is_set()
3845

3946

47+
@pytest.mark.parametrize("as_weakref", [False, True], ids=["strongref", "weakref"])
4048
@background_task()
41-
def test_async_background_executor_submit_propagates_context(loop):
49+
def test_async_background_executor_submit_propagates_context(loop, as_weakref):
4250
trace = current_trace()
4351
test_ran = Event()
4452

45-
async def task():
53+
async def task(arg1):
4654
assert current_trace() is trace
55+
assert arg1 == "test"
4756
test_ran.set()
4857

4958
async def _test():
5059
async with AsyncBackgroundExecutor(config={}) as submit:
51-
future = submit(task)
60+
if as_weakref:
61+
# Derefence the weakmethod to ensure we don't cause a crash
62+
submit = weakref.WeakMethod(submit)()
63+
future = submit(task, "test")
5264
await asyncio.wrap_future(future)
5365

5466
loop.run_until_complete(_test())

0 commit comments

Comments
 (0)