Repository: ros/ros_comm

Issue:
Crash under indigo due to concurrent rospy.get_param access
This program crashes when run under ros indigo

``` python
import rospy
import tf

rospy.init_node("test")
tf.TransformListener()
while not rospy.is_shutdown():
    p = rospy.get_param("~test", 0.5)
```

The crash is due to concurrent calls to `rospy.get_param()`, one in the test code and
one inside [SubscriberStatisticsLogger](https://github.com/ros/ros_comm/blob/indigo-devel/clients/rospy/src/rospy/impl/statistics.py#L64). TransformListener subscribes to tf in a new [thread](https://github.com/ros/geometry/blob/indigo-devel/tf/src/tf/listener.py#L238) which causes two requests to be simultaneously issued to the same http connection object.

`rospy.get_param()` is documented to be non-threadsafe, but I am not so
sure about `rospy.Subscriber()`. If `Subscriber()` should be thread safe,
then `SubscriberStatisticsLogger` needs to be modified to avoid
concurrent access to the same http connection object as the rest of
the node. Perhaps it could be given its own connection to the
parameter server? If `Subscriber()` is not intended to be threadsafe,
then `TransformListenerThread` should probably subscribe in `__init__()` instead
of `run()`.



Discussion:
From the comment it sounds like it's a regression from #383 

@contradict Can you please post the error you get when running the program?

@forouher FYI since the problem is related to the new statistics code.

Reported error is:

```
$ ./test.py 
Traceback (most recent call last):
  File "./test.py", line 8, in <module>
    p = rospy.get_param("~test", 0.5)
  File "/opt/ros/indigo/lib/python2.7/dist-packages/rospy/client.py", line 452, in get_param
    return _param_server[param_name] #MasterProxy does all the magic for us
  File "/opt/ros/indigo/lib/python2.7/dist-packages/rospy/msproxy.py", line 115, in __getitem__
    code, msg, value = self.target.getParam(rospy.names.get_caller_id(), resolved_key)
  File "/usr/lib/python2.7/xmlrpclib.py", line 1224, in __call__
    return self.__send(self.__name, args)
  File "/usr/lib/python2.7/xmlrpclib.py", line 1578, in __request
    verbose=self.__verbose
  File "/usr/lib/python2.7/xmlrpclib.py", line 1264, in request
    return self.single_request(host, handler, request_body, verbose)
  File "/usr/lib/python2.7/xmlrpclib.py", line 1289, in single_request
    self.send_request(h, handler, request_body)
  File "/usr/lib/python2.7/xmlrpclib.py", line 1391, in send_request
    connection.putrequest("POST", handler, skip_accept_encoding=True)
  File "/usr/lib/python2.7/httplib.py", line 871, in putrequest
    raise CannotSendRequest()
httplib.CannotSendRequest
$ 
```

rosparam is non thread-safe while Subscriber is supposed to be? I'm not sure about the locking model in rospy. topics.py didn't have much locking (except it's own stuff), so I just assumed to be safe with the TopicStatistics stuff.

What would be the right thing to do? Never call getParam outside the main thread?


Resolving patch:
diff --git a/clients/rospy/src/rospy/msproxy.py b/clients/rospy/src/rospy/msproxy.py
--- a/clients/rospy/src/rospy/msproxy.py
+++ b/clients/rospy/src/rospy/msproxy.py
@@ -38,7 +38,9 @@
 APIs by automatically inserting the caller ID and also adding python
 dictionary accessors on the parameter server.
 """
-    
+
+from threading import Lock
+
 import rospy.core
 import rospy.exceptions
 import rospy.names
@@ -75,6 +77,7 @@ class MasterProxy(object):
     
       master[key] = value
 
+    All methods are thread-safe.
     """
 
     def __init__(self, uri):
@@ -84,9 +87,11 @@ def __init__(self, uri):
         @type  uri: str
         """
         self.target = rospy.core.xmlrpcapi(uri)        
+        self._lock = Lock()
 
     def __getattr__(self, key): #forward api calls to target
-        f = getattr(self.target, key)
+        with self._lock:
+            f = getattr(self.target, key)
         if key in _master_arg_remap:
             remappings = _master_arg_remap[key]
         else:
@@ -112,7 +117,8 @@ def __getitem__(self, key):
         #NOTE: remapping occurs here!
         resolved_key = rospy.names.resolve_name(key)
         if 1: # disable param cache
-            code, msg, value = self.target.getParam(rospy.names.get_caller_id(), resolved_key)
+            with self._lock:
+                code, msg, value = self.target.getParam(rospy.names.get_caller_id(), resolved_key)
             if code != 1: #unwrap value with Python semantics
                 raise KeyError(key)
             return value
@@ -122,7 +128,8 @@ def __getitem__(self, key):
             return rospy.impl.paramserver.get_param_server_cache().get(resolved_key)
         except KeyError:
             # first access, make call to parameter server
-            code, msg, value = self.target.subscribeParam(rospy.names.get_caller_id(), rospy.core.get_node_uri(), resolved_key)
+            with self._lock:
+                code, msg, value = self.target.subscribeParam(rospy.names.get_caller_id(), rospy.core.get_node_uri(), resolved_key)
             if code != 1: #unwrap value with Python semantics
                 raise KeyError(key)
             # set the value in the cache so that it's marked as subscribed
@@ -137,7 +144,8 @@ def __setitem__(self, key, val):
         @param val: parameter value
         @type val: XMLRPC legal value
         """
-        self.target.setParam(rospy.names.get_caller_id(), rospy.names.resolve_name(key), val)
+        with self._lock:
+            self.target.setParam(rospy.names.get_caller_id(), rospy.names.resolve_name(key), val)
         
     def search_param(self, key):
         """
@@ -150,7 +158,8 @@ def search_param(self, key):
         mappings = rospy.names.get_mappings()
         if key in mappings:
             key = mappings[key]
-        code, msg, val = self.target.searchParam(rospy.names.get_caller_id(), key)
+        with self._lock:
+            code, msg, val = self.target.searchParam(rospy.names.get_caller_id(), key)
         if code == 1:
             return val
         elif code == -1:
@@ -165,7 +174,8 @@ def __delitem__(self, key):
         @raise ROSException: if parameter server reports an error
         """
         resolved_key = rospy.names.resolve_name(key)
-        code, msg, _ = self.target.deleteParam(rospy.names.get_caller_id(), resolved_key)
+        with self._lock:
+            code, msg, _ = self.target.deleteParam(rospy.names.get_caller_id(), resolved_key)
         if code == -1:
             raise KeyError(key)
         elif code != 1:
@@ -181,7 +191,8 @@ def __contains__(self, key):
         @type key: str
         @raise ROSException: if parameter server reports an error
         """        
-        code, msg, value = self.target.hasParam(rospy.names.get_caller_id(), rospy.names.resolve_name(key))
+        with self._lock:
+            code, msg, value = self.target.hasParam(rospy.names.get_caller_id(), rospy.names.resolve_name(key))
         if code != 1:
             raise rospy.exceptions.ROSException("cannot check parameter on server: %s"%msg)
         return value
@@ -190,7 +201,8 @@ def __iter__(self):
         """
         @raise ROSException: if parameter server reports an error
         """
-        code, msg, value = self.target.getParamNames(rospy.names.get_caller_id())
+        with self._lock:
+            code, msg, value = self.target.getParamNames(rospy.names.get_caller_id())
         if code == 1:
             return value.__iter__()
         else:


Test patch: