Repository: python-telegram-bot/python-telegram-bot

Issue:
ConversationHandler is not thread safe
The following are initial findings, but it's a good place to start:

Members which need a mutex protecting them:
- [x] `conversations`
- [x] `timeout_jobs`

Members which should be removed. Instead `check_update()` should probably return a context to be passed to `handle_updates()`:
- [x]  `current_conversation`
- [x]  `current_handler`

- [x] All other members must be set internal (i.e. renamed to be prefixed with `__`) so they won't be modified after `__init__()`.



Discussion:
`current_conversation` and `current_handler` could also be in a `threading.local`
The issues with `current_conversation` and `current_handler`  have been fixed by implementing context based callbacks (#1100) (see `ConversationHandler.check_update`).

Resolving patch:
diff --git a/telegram/ext/conversationhandler.py b/telegram/ext/conversationhandler.py
--- a/telegram/ext/conversationhandler.py
+++ b/telegram/ext/conversationhandler.py
@@ -165,23 +165,23 @@ def __init__(self,
                  persistent=False,
                  map_to_parent=None):
 
-        self.entry_points = entry_points
-        self.states = states
-        self.fallbacks = fallbacks
-
-        self.allow_reentry = allow_reentry
-        self.per_user = per_user
-        self.per_chat = per_chat
-        self.per_message = per_message
-        self.conversation_timeout = conversation_timeout
-        self.name = name
+        self._entry_points = entry_points
+        self._states = states
+        self._fallbacks = fallbacks
+
+        self._allow_reentry = allow_reentry
+        self._per_user = per_user
+        self._per_chat = per_chat
+        self._per_message = per_message
+        self._conversation_timeout = conversation_timeout
+        self._name = name
         if persistent and not self.name:
             raise ValueError("Conversations can't be persistent when handler is unnamed.")
         self.persistent = persistent
         self._persistence = None
         """:obj:`telegram.ext.BasePersistance`: The persistence used to store conversations.
         Set by dispatcher"""
-        self.map_to_parent = map_to_parent
+        self._map_to_parent = map_to_parent
 
         self.timeout_jobs = dict()
         self._timeout_jobs_lock = Lock()
@@ -225,6 +225,87 @@ def __init__(self,
                                   "since inline queries have no chat context.")
                     break
 
+    @property
+    def entry_points(self):
+        return self._entry_points
+
+    @entry_points.setter
+    def entry_points(self, value):
+        raise ValueError('You can not assign a new value to entry_points after initialization.')
+
+    @property
+    def states(self):
+        return self._states
+
+    @states.setter
+    def states(self, value):
+        raise ValueError('You can not assign a new value to states after initialization.')
+
+    @property
+    def fallbacks(self):
+        return self._fallbacks
+
+    @fallbacks.setter
+    def fallbacks(self, value):
+        raise ValueError('You can not assign a new value to fallbacks after initialization.')
+
+    @property
+    def allow_reentry(self):
+        return self._allow_reentry
+
+    @allow_reentry.setter
+    def allow_reentry(self, value):
+        raise ValueError('You can not assign a new value to allow_reentry after initialization.')
+
+    @property
+    def per_user(self):
+        return self._per_user
+
+    @per_user.setter
+    def per_user(self, value):
+        raise ValueError('You can not assign a new value to per_user after initialization.')
+
+    @property
+    def per_chat(self):
+        return self._per_chat
+
+    @per_chat.setter
+    def per_chat(self, value):
+        raise ValueError('You can not assign a new value to per_chat after initialization.')
+
+    @property
+    def per_message(self):
+        return self._per_message
+
+    @per_message.setter
+    def per_message(self, value):
+        raise ValueError('You can not assign a new value to per_message after initialization.')
+
+    @property
+    def conversation_timeout(self):
+        return self._conversation_timeout
+
+    @conversation_timeout.setter
+    def conversation_timeout(self, value):
+        raise ValueError('You can not assign a new value to conversation_timeout after '
+                         'initialization.')
+
+    @property
+    def name(self):
+        return self._name
+
+    @name.setter
+    def name(self, value):
+        raise ValueError('You can not assign a new value to name after initialization.')
+
+    @property
+    def map_to_parent(self):
+        return self._map_to_parent
+
+    @map_to_parent.setter
+    def map_to_parent(self, value):
+        raise ValueError('You can not assign a new value to map_to_parent after initialization.')
+
     @property
     def persistence(self):
         return self._persistence


Test patch:
diff --git a/tests/test_conversationhandler.py b/tests/test_conversationhandler.py
--- a/tests/test_conversationhandler.py
+++ b/tests/test_conversationhandler.py
@@ -179,6 +179,38 @@ def stop(self, bot, update):
         return self._set_state(update, self.STOPPING)
 
     # Tests
+    @pytest.mark.parametrize('attr', ['entry_points', 'states', 'fallbacks', 'per_chat', 'name',
+                             'per_user', 'allow_reentry', 'conversation_timeout', 'map_to_parent'],
+                             indirect=False)
+    def test_immutable(self, attr):
+        ch = ConversationHandler('entry_points', {'states': ['states']}, 'fallbacks',
+                                 per_chat='per_chat',
+                                 per_user='per_user', per_message=False,
+                                 allow_reentry='allow_reentry',
+                                 conversation_timeout='conversation_timeout',
+                                 name='name', map_to_parent='map_to_parent')
+
+        value = getattr(ch, attr)
+        if isinstance(value, list):
+            assert value[0] == attr
+        elif isinstance(value, dict):
+            assert list(value.keys())[0] == attr
+        else:
+            assert getattr(ch, attr) == attr
+        with pytest.raises(ValueError, match='You can not assign a new value to {}'.format(attr)):
+            setattr(ch, attr, True)
+
+    def test_immutable_per_message(self):
+        ch = ConversationHandler('entry_points', {'states': ['states']}, 'fallbacks',
+                                 per_chat='per_chat',
+                                 per_user='per_user', per_message=False,
+                                 allow_reentry='allow_reentry',
+                                 conversation_timeout='conversation_timeout',
+                                 name='name', map_to_parent='map_to_parent')
+        assert ch.per_message is False
+        with pytest.raises(ValueError, match='You can not assign a new value to per_message'):
+            ch.per_message = True
+
     def test_per_all_false(self):
         with pytest.raises(ValueError, match="can't all be 'False'"):
             ConversationHandler(self.entry_points, self.states, self.fallbacks,