Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rospy: added get_param_cached #1515

Merged
merged 7 commits into from
Feb 8, 2020

Conversation

mgrrx
Copy link
Contributor

@mgrrx mgrrx commented Sep 28, 2018

rospy had an incomplete version of a local parameter cache in the code base. The code was never called due to https://github.com/ros/ros_comm/blob/melodic-devel/clients/rospy/src/rospy/msproxy.py#L119 .

I borrowed some of the code in rosmaster and added the rospy toplevel function get_param_cached (in accordance to roscpp::getParamCached). It was surprisingly easy since the subscribeParam functionality was already there.

@NickZ
Copy link

NickZ commented Jan 11, 2019

Is there anything blocking merging on this? This brings the rospy client in-line with the behavior of roscpp client, and will solve a number of performance issues that I am seeing with rospy client.

@mgrrx
Copy link
Contributor Author

mgrrx commented Jan 12, 2019

This PR was labeled as an enhancement and did not receive further feedback. It would be great if you could test this feature and report feedback.
I guess there should also be some discussion on wether the caching should happen by default (as it was thought to be in the original code) or if the extra method get_param_cached should be introduced. I personally decided for the latter since it is the same in roscpp.

@paulbovbel
Copy link
Contributor

We've been running this patch w/ locusrobotics#10 at Locus since February with success. Thanks @mgrrx!

@furushchev
Copy link
Contributor

@mgrrx Hi, thank you very much for the patch!
I'm struggling with using rospy in environment with latency between rosmaster and rospy client node, and the change in this pull request completely solves the issue I have.
It would be nice if the feature is merged. 👍

( I tested the patch in #1855 )

IMO, It would be better to append get_param_cached instead of changing get_param, since behavior of existing nodes are changed.

@flixr
Copy link
Contributor

flixr commented Jan 12, 2020

@ros-pull-request-builder retest this please

@mgrrx
Copy link
Contributor Author

mgrrx commented Feb 6, 2020

any chance to get this one in melodic?

@dirk-thomas
Copy link
Member

@ros-pull-request-builder retest this please

@dirk-thomas
Copy link
Member

Thanks for the improvement.

@dirk-thomas dirk-thomas merged commit d16296a into ros:melodic-devel Feb 8, 2020
howardcochran pushed a commit to BadgerTechnologies/ros_comm that referenced this pull request Nov 22, 2021
This bug is severe because it can break ordinary rospy.get_param()
after a rospy.get_param_cached() has been perfomed. And, furthermore,
rospy.log*() does a rospy.get_param_cached()!

When a parameter is set in the cache, the cached value will always be
returned, even when the caller uses ordinary rospy.get_param(). This is
as designed, as subscribeParam() is done to make the cache
receive updates from the param server whenever the value changes.

However, existing code only uses the presence of a key in the cache to
indicate whether the cached param is valid. However, this is not enough.

Suppose the param server has these contents:
    top1:
        min: 1
        max: 10

    print(rospy.get_param('/top'))
    # So far, so good. Output is:
    # {'min': 1, 'max': 10}

    print(rospy.get_param_cached('/top/min'))
    # As expected, this outputs:
    # 1
    # However, ***'s cache now contains:
    # {'top': {'min': 1}}
    # This is correct, because only the value of 'min' is cached.

    print(rospy.get_param('/top'))
    # Incorrect output:
    # {'top': {'min': 1}}
    # Because ****'s cache makes it look like /top is in the cache, but
    # it was never actually cached.

The fix is to track the actual keys that are cached in a set. Upon, only
return a cached value if either the key or a parent of it is in the set.

This was found by productioncode which attempts to get all params using
rospy.get_param('/'). This works OK as long as no calls to
rospy.get_param_cached() have been made. However, the rospy.log*()
functions all do
rospy.get_param_cached('rosout_disable_topics_generation'). So after the
first loginfo, subsequent calls to rospy.get_param('/') only the single
cached key rather than all params.

Fixes: d16296a ("rospy: added get_param_cached (ros#1515)")
howardcochran pushed a commit to BadgerTechnologies/ros_comm that referenced this pull request Nov 23, 2021
This bug is severe because it can break ordinary rospy.get_param()
after a rospy.get_param_cached() has been perfomed. And, furthermore,
rospy.log*() does a rospy.get_param_cached()!

When a parameter is set in the cache, the cached value will always be
returned, even when the caller uses ordinary rospy.get_param(). This is
as designed, as subscribeParam() is done to make the cache
receive updates from the param server whenever the value changes.

However, existing code only uses the presence of a key in the cache to
indicate whether the cached param is valid. However, this is not enough.

Suppose the param server has these contents:
    top:
        min: 1
        max: 10

    print(rospy.get_param('/top'))
    # So far, so good. Output is:
    # {'min': 1, 'max': 10}

    print(rospy.get_param_cached('/top/min'))
    # As expected, this outputs:
    # 1
    # However, rospy.MastProxy's cache now contains:
    # {'top': {'min': 1}}
    # This is correct, because only the value of 'min' is cached.

    print(rospy.get_param('/top'))
    # Incorrect output:
    # {'min': 1}
    # Ugh! 'max': 10 is missing!
    # Because rospy.MastProxy's cache makes it look like /top is in the
    # cache, but it was never actually cached; only the individual item
    # within it, 'min', was cached.

The fix is to track the actual keys that are cached in a Python set().
Only return a cached value if either the key or a parent of it is in
the set.

This was found by production code which attempts to get all params using
rospy.get_param('/'). This works OK as long as no calls to
rospy.get_param_cached() have been made. However, the rospy.log*()
functions all do
rospy.get_param_cached('rosout_disable_topics_generation'). So after the
first loginfo, subsequent calls to rospy.get_param('/') return only the
single cached key rather than all params.

Fixes: d16296a ("rospy: added get_param_cached (ros#1515)")
howardcochran pushed a commit to BadgerTechnologies/ros_comm that referenced this pull request Nov 23, 2021
If rospy.get_param_cached() is called with a non-existent parameter,
existing code would attempt to encode that the parameter doesn't exist
by storing its value in the cache as an empty dictionary. And it would
correctly make a subscribeParam call to rosmaster to receive updates.

However, every subsequent .get_param_cached()
on this non-existent parameter would re-subscribe for updates from
rosmaster, nullifying any performance benefit from caching. Likewise, a
regular .get_param() would simply re-read the parameter from rosmaster
every time, still not benefitting from caching.

To make matters worse, the rospy.log* functions do a
rospy.get_param_cached('rosout_disable_topics_generation'), so we pay
this penalty on every log message whenever that paremeter is not set (it
is not set by default!).

The cause of this problem is that the translation of an empty dict in
the cache to a KeyError occurs within the cache itself, so the
caller can't know whether the parameter is simply unset vs. not
subscribed for updates.

The fix is simple: Move the check for empty dict from the cache itself
to the caller.

Fixes: d16296a ("rospy: added get_param_cached (ros#1515)")
howardcochran pushed a commit to BadgerTechnologies/ros_comm that referenced this pull request Nov 23, 2021
This bug is severe because it can break ordinary rospy.get_param()
after a rospy.get_param_cached() has been perfomed. And, furthermore,
rospy.log*() does a rospy.get_param_cached()!

When a parameter is set in the cache, the cached value will always be
returned, even when the caller uses ordinary rospy.get_param(). This is
as designed, as subscribeParam() is done to make the cache
receive updates from the param server whenever the value changes.

However, existing code only uses the presence of a key in the cache to
indicate whether the cached param is valid. However, this is not enough.

Suppose the param server has these contents:
    top:
        min: 1
        max: 10

    print(rospy.get_param('/top'))
    # So far, so good. Output is:
    # {'min': 1, 'max': 10}

    print(rospy.get_param_cached('/top/min'))
    # As expected, this outputs:
    # 1
    # However, ParamServerCache now contains:
    # {'top': {'min': 1}}
    # This is correct, because only the value of 'min' is cached.

    print(rospy.get_param('/top'))
    # Incorrect output:
    # {'min': 1}
    # Ugh! 'max': 10 is missing!
    # ...because it looks like /top is in the cache, but it was never
    # actually cached; only the individual item within it, 'min', was
    # cached.

The fix is to track the actual keys that are cached in a Python set().
Only return a cached value if either the key or a parent of it is in
the set.

This was found by production code which attempts to get all params using
rospy.get_param('/'). This works OK as long as no calls to
rospy.get_param_cached() have been made. However, the rospy.log*()
functions all do
rospy.get_param_cached('rosout_disable_topics_generation'). So after the
first loginfo, subsequent calls to rospy.get_param('/') return only the
single cached key rather than all params.

Fixes: d16296a ("rospy: added get_param_cached (ros#1515)")
howardcochran pushed a commit to BadgerTechnologies/ros_comm that referenced this pull request Nov 23, 2021
If rospy.get_param_cached() is called with a non-existent parameter,
existing code would attempt to encode that the parameter doesn't exist
by storing its value in the cache as an empty dictionary. And it would
correctly make a subscribeParam call to rosmaster to receive updates.

However, every subsequent .get_param_cached()
on this non-existent parameter would re-subscribe for updates from
rosmaster, nullifying any performance benefit from caching. Likewise, a
regular .get_param() would simply re-read the parameter from rosmaster
every time, still not benefitting from caching.

To make matters worse, the rospy.log* functions do a
rospy.get_param_cached('rosout_disable_topics_generation'), so we pay
this penalty on every log message whenever that paremeter is not set (it
is not set by default!).

The cause of this problem is that the translation of an empty dict in
the cache to a KeyError occurs within the cache itself, so the
caller can't know whether the parameter is simply unset vs. not
subscribed for updates.

The fix is simple: Move the check for empty dict from the cache itself
to the caller.

Fixes: d16296a ("rospy: added get_param_cached (ros#1515)")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants