Repository: rollbar/pyrollbar

Issue:
Exception while reporting exc_info to Rollbar. TypeError("can't pickle select.epoll objects",)
I'm using `rollbar==0.13.16` with `tornado==4.5.2` and getting the following exception:

```
[E 171025 05:47:48 __init__:411] Exception while reporting message to Rollbar. TypeError("can't pickle select.epoll objects",)
    Traceback (most recent call last):
      File "/usr/local/lib/python3.6/site-packages/tornado/web.py", line 1511, in _execute
        result = yield result
      File "/usr/local/lib/python3.6/site-packages/tornado/gen.py", line 1055, in run
        value = future.result()
      File "/usr/local/lib/python3.6/site-packages/tornado/concurrent.py", line 238, in result
        raise_exc_info(self._exc_info)
      File "<string>", line 4, in raise_exc_info
      File "/usr/local/lib/python3.6/site-packages/tornado/gen.py", line 1063, in run
        yielded = self.gen.throw(*exc_info)
      File "/project-ps-hsm/src/handlers/balance.py", line 21, in get
        yield self.check_blacklist()
      File "/usr/local/lib/python3.6/site-packages/tornado/gen.py", line 1055, in run
        value = future.result()
      File "/usr/local/lib/python3.6/site-packages/tornado/concurrent.py", line 238, in result
        raise_exc_info(self._exc_info)
      File "<string>", line 4, in raise_exc_info
      File "/usr/local/lib/python3.6/site-packages/tornado/gen.py", line 1063, in run
        yielded = self.gen.throw(*exc_info)
      File "/project-ps-hsm/src/handlers/_base.py", line 81, in check_blacklist
        reason = yield is_blacklisted(self.can, self.current_user)
      File "/usr/local/lib/python3.6/site-packages/tornado/gen.py", line 1055, in run
        value = future.result()
      File "/usr/local/lib/python3.6/site-packages/tornado/concurrent.py", line 238, in result
        raise_exc_info(self._exc_info)
      File "<string>", line 4, in raise_exc_info
      File "/usr/local/lib/python3.6/site-packages/tornado/gen.py", line 1063, in run
        yielded = self.gen.throw(*exc_info)
      File "/project-ps-hsm/src/utils/afp_processing.py", line 42, in is_blacklisted
        'SECRET': config.INTERNAL_API_SECRET['customer'],
      File "/usr/local/lib/python3.6/site-packages/tornado/gen.py", line 1055, in run
        value = future.result()
      File "/usr/local/lib/python3.6/site-packages/tornado/concurrent.py", line 238, in result
        raise_exc_info(self._exc_info)
      File "<string>", line 4, in raise_exc_info
    tornado.httpclient.HTTPError: HTTP 500: Internal Server Error
    
    During handling of the above exception, another exception occurred:
    
    Traceback (most recent call last):
      File "/usr/local/lib/python3.6/site-packages/rollbar/__init__.py", line 409, in report_message
        return _report_message(message, level, request, extra_data, payload_data)
      File "/usr/local/lib/python3.6/site-packages/rollbar/__init__.py", line 712, in _report_message
        data = dict_merge(data, payload_data)
      File "/usr/local/lib/python3.6/site-packages/rollbar/lib/__init__.py", line 174, in dict_merge
        result[k] = dict_merge(result[k], v)
      File "/usr/local/lib/python3.6/site-packages/rollbar/lib/__init__.py", line 176, in dict_merge
        result[k] = copy.deepcopy(v)
      File "/usr/local/lib/python3.6/copy.py", line 150, in deepcopy
        y = copier(x, memo)
      File "/usr/local/lib/python3.6/copy.py", line 215, in _deepcopy_list
        append(deepcopy(a, memo))
      File "/usr/local/lib/python3.6/copy.py", line 150, in deepcopy
        y = copier(x, memo)
      File "/usr/local/lib/python3.6/copy.py", line 240, in _deepcopy_dict
        y[deepcopy(key, memo)] = deepcopy(value, memo)
      File "/usr/local/lib/python3.6/copy.py", line 150, in deepcopy
        y = copier(x, memo)
      File "/usr/local/lib/python3.6/copy.py", line 220, in _deepcopy_tuple
        y = [deepcopy(a, memo) for a in x]
      File "/usr/local/lib/python3.6/copy.py", line 220, in <listcomp>
        y = [deepcopy(a, memo) for a in x]
      File "/usr/local/lib/python3.6/copy.py", line 180, in deepcopy
        y = _reconstruct(x, memo, *rv)
      File "/usr/local/lib/python3.6/copy.py", line 280, in _reconstruct
        state = deepcopy(state, memo)
      File "/usr/local/lib/python3.6/copy.py", line 150, in deepcopy
        y = copier(x, memo)
      File "/usr/local/lib/python3.6/copy.py", line 240, in _deepcopy_dict
        y[deepcopy(key, memo)] = deepcopy(value, memo)
      File "/usr/local/lib/python3.6/copy.py", line 180, in deepcopy
        y = _reconstruct(x, memo, *rv)
      File "/usr/local/lib/python3.6/copy.py", line 280, in _reconstruct
        state = deepcopy(state, memo)
      File "/usr/local/lib/python3.6/copy.py", line 150, in deepcopy
        y = copier(x, memo)
      File "/usr/local/lib/python3.6/copy.py", line 240, in _deepcopy_dict
        y[deepcopy(key, memo)] = deepcopy(value, memo)
      File "/usr/local/lib/python3.6/copy.py", line 180, in deepcopy
        y = _reconstruct(x, memo, *rv)
      File "/usr/local/lib/python3.6/copy.py", line 280, in _reconstruct
        state = deepcopy(state, memo)
      File "/usr/local/lib/python3.6/copy.py", line 150, in deepcopy
        y = copier(x, memo)
      File "/usr/local/lib/python3.6/copy.py", line 240, in _deepcopy_dict
        y[deepcopy(key, memo)] = deepcopy(value, memo)
      File "/usr/local/lib/python3.6/copy.py", line 180, in deepcopy
        y = _reconstruct(x, memo, *rv)
      File "/usr/local/lib/python3.6/copy.py", line 280, in _reconstruct
        state = deepcopy(state, memo)
      File "/usr/local/lib/python3.6/copy.py", line 150, in deepcopy
        y = copier(x, memo)
      File "/usr/local/lib/python3.6/copy.py", line 240, in _deepcopy_dict
        y[deepcopy(key, memo)] = deepcopy(value, memo)
      File "/usr/local/lib/python3.6/copy.py", line 169, in deepcopy
        rv = reductor(4)
    TypeError: can't pickle select.epoll objects
```

It results in certain errors don't show up in Rollbar which is pretty bad 😞 
It seems like not every `exc_info` is pickable and this situation should be handled properly. I suggest still reporting the error message, even without the exception info.

Discussion:
rokob: It turns out that copy/deepcopy go through the pickle machinery via __reduce__ and __reduce_ex__, so things that are not copyable throw TypeErrors from pickling.

Patch:
diff --git a/rollbar/lib/__init__.py b/rollbar/lib/__init__.py
index 1afaee1..9a3f664 100644
--- a/rollbar/lib/__init__.py
+++ b/rollbar/lib/__init__.py
@@ -173,7 +173,10 @@ def dict_merge(a, b):
         if k in result and isinstance(result[k], dict):
             result[k] = dict_merge(result[k], v)
         else:
-            result[k] = copy.deepcopy(v)
+            try:
+                result[k] = copy.deepcopy(v)
+            except:
+                result[k] = '<Uncopyable obj:(%s)>' % (v,)
 
     return result
 


Test patch:
diff --git a/rollbar/test/test_lib.py b/rollbar/test/test_lib.py
new file mode 100644
index 0000000..201a2ed
--- /dev/null
+++ b/rollbar/test/test_lib.py
@@ -0,0 +1,59 @@
+from rollbar.lib import dict_merge
+
+from rollbar.test import BaseTest
+
+class RollbarLibTest(BaseTest):
+    def test_dict_merge_not_dict(self):
+        a = {'a': {'b': 42}}
+        b = 99
+        result = dict_merge(a, b)
+
+        self.assertEqual(99, result)
+
+    def test_dict_merge_dicts_independent(self):
+        a = {'a': {'b': 42}}
+        b = {'x': {'y': 99}}
+        result = dict_merge(a, b)
+
+        self.assertIn('a', result)
+        self.assertIn('b', result['a'])
+        self.assertEqual(42, result['a']['b'])
+        self.assertIn('x', result)
+        self.assertIn('y', result['x'])
+        self.assertEqual(99, result['x']['y'])
+
+    def test_dict_merge_dicts(self):
+        a = {'a': {'b': 42}}
+        b = {'a': {'c': 99}}
+        result = dict_merge(a, b)
+
+        self.assertIn('a', result)
+        self.assertIn('b', result['a'])
+        self.assertIn('c', result['a'])
+        self.assertEqual(42, result['a']['b'])
+        self.assertEqual(99, result['a']['c'])
+
+    def test_dict_merge_dicts_second_wins(self):
+        a = {'a': {'b': 42}}
+        b = {'a': {'b': 99}}
+        result = dict_merge(a, b)
+
+        self.assertIn('a', result)
+        self.assertIn('b', result['a'])
+        self.assertEqual(99, result['a']['b'])
+
+    def test_dict_merge_dicts_select_poll(self):
+        import select
+        poll = getattr(select, 'poll', None)
+        if poll is None:
+            return
+        p = poll()
+        a = {'a': {'b': 42}}
+        b = {'a': {'y': p}}
+        result = dict_merge(a, b)
+
+        self.assertIn('a', result)
+        self.assertIn('b', result['a'])
+        self.assertEqual(42, result['a']['b'])
+        self.assertIn('y', result['a'])
+        self.assertRegex(result['a']['y'], r'Uncopyable obj')