Repository: getappmap/appmap-python (language: python)

Issue:
Instrumentation breaks code that uses the multiprocessing module
Trying to create an AppMap of code that uses the `multiprocessing` module raises an exception.

For example, with a project that looks like this:
```
mp
├── __init__.py
├── hw.py
└── mp.py
```
where `mp/hw.py` contains this
```python
def hw(d):
    d["ret"] = "hello world!"
    print(d["ret"])
```
and `mp/mp.py` contains this
```python
import appmap
from multiprocessing import Manager, Process
from .hw import hw

def do_it():
    with Manager() as manager:
        d = manager.dict()
        p = Process(target=hw, args=(d,))
        p.start()
        p.join()
        return d["ret"]


if __name__ == "__main__":
    r = appmap.Recording()
    with r:
        do_it()
    print(appmap.generation.dump(r))
```
running the command
```shell
$ python -m mp.mp
```
raises an exception that ends like this:
```
  File "/Users/ajp/.asdf/installs/python/3.10.8/lib/python3.10/pickle.py", line 631, in save
    self.save_reduce(obj=obj, *rv)
  File "/Users/ajp/.asdf/installs/python/3.10.8/lib/python3.10/pickle.py", line 729, in save_reduce
    save(func)
  File "/Users/ajp/.asdf/installs/python/3.10.8/lib/python3.10/pickle.py", line 598, in save
    self.save_global(obj)
  File "/Users/ajp/.asdf/installs/python/3.10.8/lib/python3.10/pickle.py", line 1120, in save_global
    __import__(module_name, level=0)
TypeError: __import__() argument 1 must be str, not property
```

(`multiprocessing` is trying to pickle an `appmap.wrapt.FunctionWrapper`. Because of the way `FunctionWrapper` is implemented, `FunctionWrapper.__module__` is a `property` not a `str`, so pickling fails.)



Discussion / hints:
It's possible to workaround this issue by telling `multiprocessing` how to pickle a `FunctionWrapper`. Putting this code in the appropriate place (e.g. `mp/__init__.py` in the simple example above, or in `conftest.py` if you're recording pytest tests) will make it stop happening:
```python
from appmap.wrapt import FunctionWrapper

def fp_reducer(self, obj):
    name = getattr(obj, "__name__", None)

    if type(obj) == FunctionWrapper:
        return obj.__qualname__

    return NotImplemented


from multiprocessing.reduction import ForkingPickler

ForkingPickler.reducer_override = fp_reducer
```

While this avoids the exception, until #201 is fixed, capturing a recording the function passed to `multiprocessing` must be done manually (e.g. using a `Recording` as a context manager).

Fix patch:
diff --git a/vendor/wrapt b/vendor/wrapt
index fb8fc6c1..7dcec811 160000
--- a/vendor/wrapt
+++ b/vendor/wrapt
@@ -1,1 +1,1 @@
-Subproject commit fb8fc6c1ac75c6dad4642239f737de39be6632c2
+Subproject commit 7dcec8118747b8a480c1664cad3c16ab9e86f538


Test patch:
diff --git a/appmap/test/test_recording.py b/appmap/test/test_recording.py
index cbc6a4f3..812ab4d6 100644
--- a/appmap/test/test_recording.py
+++ b/appmap/test/test_recording.py
@@ -10,6 +10,7 @@
 import appmap
 from appmap._implementation.event import Event
 from appmap._implementation.recorder import Recorder, ThreadRecorder
+from appmap.wrapt import FunctionWrapper
 
 from .normalize import normalize_appmap, remove_line_numbers
 
@@ -110,9 +111,22 @@ def test_can_deepcopy_function(self):
 
         rec = appmap.Recording()
         with rec:
+            assert isinstance(modfunc, FunctionWrapper)
             f1 = deepcopy(modfunc)
             f1()
 
+    def test_can_pickle(self):
+        import pickle
+
+        from example_class import (  # pyright: ignore[reportMissingImports] pylint: disable=import-error
+            modfunc,
+        )
+
+        rec = appmap.Recording()
+        with rec:
+            assert isinstance(modfunc, FunctionWrapper)
+            f = pickle.loads(pickle.dumps(modfunc))
+            f()
         evt = rec.events[-2]
         assert evt.event == "call"
         assert evt.method_id == "modfunc"