-
Notifications
You must be signed in to change notification settings - Fork 0
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
QoS - API and implemention for event callbacks #6
Conversation
For folks looking at this here's a demo app i've been using to try it out
|
b6f4478
to
0f8b8b0
Compare
@thomas-moulard I think this is ready (except for one last TODO that I am tracking down) it seems like you've got a bit of experience with C-Python interfaces, can you give this a once-over review before I open up the review to openrobotics? |
0f8b8b0
to
ae0f5eb
Compare
rclpy/rclpy/node.py
Outdated
qos_profile: QoSProfile = qos_profile_default | ||
qos_profile: QoSProfile = qos_profile_default, | ||
callback_group: CallbackGroup = None, | ||
event_callbacks: PublisherEventCallbacks = PublisherEventCallbacks(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One of the subtleties of default parameters in Python is that they are only evaluated once when the function is defined instead of every time the function is executed. This means that every call to create_publisher
will use the same default PublisherEventCallbacks()
object instance. The safe option is to use None
as the default value and then construct a new instance of the object within the function itself when providing a default for mutable objects.
I don't think it breaks anything right now, so I wouldn't consider this a blocking issue. I wanted to bring it up anyways in case I'm missing something in how it's used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch - I'll default to None
rclpy/rclpy/publisher.py
Outdated
self.event_handlers: List[QoSEventHandler] = [] | ||
if event_callbacks: | ||
if event_callbacks.deadline: | ||
self.event_handlers.append(QoSEventHandler( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Kind of a nitpick, but I think it would be better if there was a function in the PublisherEventCallbacks
class that returns a list of QoSEventHandler
objects for all the event handlers in the class. My reasoning is that if someone adds a new type of event handle in the future it is more obvious that they need to add it to the list in that class than it is this list in an unrelated file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like that idea - will do
rclpy/rclpy/subscription.py
Outdated
|
||
self.event_callbacks = event_callbacks | ||
self.event_handlers: List[QoSEventHandler] = [] | ||
if event_callbacks: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment here as in the Publisher.
rclpy/rclpy/node.py
Outdated
qos_profile: QoSProfile = qos_profile_default | ||
qos_profile: QoSProfile = qos_profile_default, | ||
callback_group: CallbackGroup = None, | ||
event_callbacks: PublisherEventCallbacks = PublisherEventCallbacks(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1
rclpy/rclpy/node.py
Outdated
@@ -348,16 +350,24 @@ def create_publisher( | |||
msg_type, | |||
topic: str, | |||
*, | |||
qos_profile: QoSProfile = qos_profile_default | |||
qos_profile: QoSProfile = qos_profile_default, | |||
callback_group: CallbackGroup = None, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
callback_group type is Optional[CallbackGroup] as None is allowed.
rclpy/rclpy/node.py
Outdated
:return: The new publisher. | ||
""" | ||
if callback_group is None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this syntax, you don't need to use it but FYI
callback_group = callback_group or self.default_callback_group
rclpy/rclpy/node.py
Outdated
@@ -380,6 +398,7 @@ def create_subscription( | |||
*, | |||
qos_profile: QoSProfile = qos_profile_default, | |||
callback_group: CallbackGroup = None, | |||
event_callbacks: SubscriptionEventCallbacks = SubscriptionEventCallbacks(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto
rclpy/rclpy/publisher.py
Outdated
self.event_callbacks = event_callbacks | ||
self.event_handlers: List[QoSEventHandler] = [] | ||
if event_callbacks: | ||
if event_callbacks.deadline: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds like this could be split into multiple functions, this starts to be a bit long
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Going to implement as function on EventCallbacks to keep centralized - as per Nick's comment below
rclpy/rclpy/qos_event.py
Outdated
|
||
Mirrors rmw_requested_deadline_missed_status_t from rmw/types.h | ||
""" | ||
QoSRequestedDeadlineMissedInfo = namedtuple( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use typed namedtuple instead: https://docs.python.org/3/library/typing.html#typing.NamedTuple
rclpy/src/rclpy/_rclpy.c
Outdated
_rclpy_destroy_event(PyObject * pycapsule) | ||
{ | ||
rcl_event_t * event = PyCapsule_GetPointer(pycapsule, "rcl_event_t"); | ||
rcl_ret_t ret_event = rcl_event_fini(event); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Order sounds wrong there, freeing the C underlying object before the python one?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is called automatically as the destructor of the allocated Capsule when it is destroyed by Python https://github.com/aws-robotics/rclpy/pull/6/files#diff-80dda03110b5be606b823d6b0558b5c2R1416
There isn't a Python object for me to free.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unless I have misunderstood your comment - and you mean event
should be freed first - but I have to send event
to fini
before that, so its members can be freed - if I were to free event
first I couldn't access its members any more to free them.
rclpy/rclpy/publisher.py
Outdated
from rclpy.impl.implementation_singleton import rclpy_implementation as _rclpy | ||
from rclpy.qos import QoSProfile | ||
from rclpy.qos_event import PublisherEventCallbacks | ||
from rclpy.qos_event import QoSEventHandler |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we both got the naming convention wrong. In rclcpp
it's QOSEventHandler
, here it's QoSEventHandler
, but the Google style guide would've wanted QosEventHandler
(https://google.github.io/styleguide/cppguide.html#General_Naming_Rules). Let's change it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The existing python codebase uses QoS
(AaA
) for the functionality that was already here. I'm going to keep it this way to match the existing convention.
See https://github.com/ros2/rclpy/blob/master/rclpy/rclpy/qos.py#L21
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should run ASAN on this PR before merging it, it's hard to get the Python ref-counting right.
rclpy/src/rclpy/_rclpy.c
Outdated
* \return NULL on failure | ||
*/ | ||
static PyObject * | ||
rclpy_create_event(PyObject * Py_UNUSED(self), PyObject * args) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Big function - can we split this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
C is pretty verbose - do you really think this is actually more than one logical unit of code?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here is a re-implementation of your function (coding style, passing precisely the args etc. is left as an exercise for the reader :)
bool _is_pycapsule_publisher(PyObject* pyobject) {
return PyCapsule_IsValid(pyparent, "rcl_subscription_t");
}
bool _is_pycapsule_subscription(PyObject* pyobject) {
return PyCapsule_IsValid(pyparent, "rcl_publisher_t");
}
rcl_event_t * _allocate_and_zero_initialize_rcl_event() {
rcl_event_t * event = (rcl_event_t *)PyMem_Malloc(sizeof(rcl_event_t));
if (!event) {
PyErr_Format(PyExc_MemoryError, "Failed to allocate memory for event");
return NULL;
}
*event = rcl_get_zero_initialized_event();
return event;
}
// forcing yourself to divide your code in smaller functions will help build
// generally useful utility functions such as this one instead of considering
// the code "write-only".
bool _check_rcl_errors_status() {
if (ret != RCL_RET_OK) {
rcl_reset_error();
PyErr_Format(PyExc_RuntimeError, rcutils_get_error_string());
return true;
} else {
return false;
}
}
bool _initialize_event_publisher(event) {
const rcl_ret_t ret = rcl_subscription_event_init(event, subscription, pyevent_type);
return _check_rcl_error_status();
}
bool _initialize_event_subscription(event) {
const rcl_ret_t ret = rcl_subscription_event_init(event, subscription, pyevent_type);
return _check_rcl_error_status();
}
bool _initialize_event(event) {
if(_is_pycapsule_publisher(event)) {
return _initialize_event_publisher();
} else if(_is_pycapsule_subscription(event)) {
return _initialize_event_subscription();
}
}
static PyObject *
rclpy_create_event(PyObject * Py_UNUSED(self), PyObject * args) {
const rcl_event_t * event = _allocate_and_zero_initialize_rcl_event();
if (!event) {
return NULL;
}
if (_initialize_event(event)) {
return event;
} else {
PyMem_Free(event); // Nesting functions will help you ensuring cleanup code is needed only ONCE.
return NULL;
}
}
rclpy/src/rclpy/_rclpy.c
Outdated
* \return NULL on failure | ||
*/ | ||
static PyObject * | ||
rclpy_take_event(PyObject * Py_UNUSED(self), PyObject * args) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Big function - can we split this?
{ | ||
"rclpy_take_event", rclpy_take_event, METH_VARARGS, | ||
"Get the pending data for a ready QoS Event." | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file looks huge, time to break into one file per python function or something? (at least partially split this)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is huge - but i'm kind of in favor of big code files for feature sets - single compilation unit usually builds faster than code with lots of headers and you always know where to find what you're looking for. Regardless - I'd be hesitant to break it out as part of this feature - that feels like its own dedicated change.
rclpy/src/rclpy_common/src/common.c
Outdated
PyObject * pyqos_event_class = NULL; | ||
PyObject * pyqos_event = NULL; | ||
|
||
pyqos_event_module = PyImport_ImportModule("rclpy.qos_event"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's have a single cleanup logic using Py_XDECREF on all variables to reduce the complexity of this code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok - I'll restructure it a bit
Additionally, this PR needs unit tests. It can be python code, but it should call directly each bound symbol one by one. |
70c321c
to
8183480
Compare
Working on adding some unit tests still - but @thomas-moulard do you like the look of it better this way? I tried to make the functions more readable, and I see the value in abstracting away some of the string-based functions, so that those potential mistakes can at least be centralized. |
8183480
to
edd4007
Compare
* Every executor gets its own SIGINT guard condition Moves signal handling code to _rclpy_signal_handler Every executor adds a guard condition to a global list SIGINT signal handler triggers all guard conditions in global list Signed-off-by: Shane Loretz <sloretz@osrfoundation.org> * _sigint_gc robust to shutdown() called twice Signed-off-by: Shane Loretz <sloretz@osrfoundation.org> * Remove redundant comments Comments say the same thing twice. It only needs to be said once. Remove extra comments so the same thing is not repeated. Signed-off-by: Shane Loretz <sloretz@osrfoundation.org> * Split loop for readability Signed-off-by: Shane Loretz <sloretz@osrfoundation.org> * g_guard_conditions atomic variable Signed-off-by: Shane Loretz <sloretz@osrfoundation.org> * Use rclutils_atomics macros Signed-off-by: Shane Loretz <sloretz@osrfoundation.org> * Call original handler before losing reference to it Signed-off-by: Shane Loretz <sloretz@osrfoundation.org> * remove extra unnecessary assignment Signed-off-by: Shane Loretz <sloretz@osrfoundation.org> * g_guard_conditions is a struct on windows Signed-off-by: Shane Loretz <sloretz@osrfoundation.org> * Rename action state transitions (ros2#300) * Rename action state transitions Now using active verbs as described in the design doc: http://design.ros2.org/articles/actions.html#goal-states Connects to ros2/rcl#399. Signed-off-by: Jacob Perron <jacob@openrobotics.org> * add missing error handling and cleanup (ros2#315) Signed-off-by: Dirk Thomas <dirk-thomas@users.noreply.github.com> * Don't store sigint_gc address Signed-off-by: Shane Loretz <sloretz@osrfoundation.org> * remove redundant conditional Signed-off-by: Shane Loretz <sloretz@osrfoundation.org> * Every executor gets its own SIGINT guard condition Moves signal handling code to _rclpy_signal_handler Every executor adds a guard condition to a global list SIGINT signal handler triggers all guard conditions in global list Signed-off-by: Shane Loretz <sloretz@osrfoundation.org> * _sigint_gc robust to shutdown() called twice Signed-off-by: Shane Loretz <sloretz@osrfoundation.org> * Remove redundant comments Comments say the same thing twice. It only needs to be said once. Remove extra comments so the same thing is not repeated. Signed-off-by: Shane Loretz <sloretz@osrfoundation.org> * Split loop for readability Signed-off-by: Shane Loretz <sloretz@osrfoundation.org> * g_guard_conditions atomic variable Signed-off-by: Shane Loretz <sloretz@osrfoundation.org> * Use rclutils_atomics macros Signed-off-by: Shane Loretz <sloretz@osrfoundation.org> * Call original handler before losing reference to it Signed-off-by: Shane Loretz <sloretz@osrfoundation.org> * remove extra unnecessary assignment Signed-off-by: Shane Loretz <sloretz@osrfoundation.org> * g_guard_conditions is a struct on windows Signed-off-by: Shane Loretz <sloretz@osrfoundation.org> * Don't store sigint_gc address Signed-off-by: Shane Loretz <sloretz@osrfoundation.org> * remove redundant conditional Signed-off-by: Shane Loretz <sloretz@osrfoundation.org>
* add tests ensuring TypeError is raised Signed-off-by: Dirk Thomas <dirk-thomas@users.noreply.github.com> * raise TypeError when passing invalid message types Signed-off-by: Dirk Thomas <dirk-thomas@users.noreply.github.com>
Signed-off-by: Emerson Knapp <eknapp@amazon.com>
Signed-off-by: Emerson Knapp <eknapp@amazon.com>
…in my test app, not in the rclpy functionality Signed-off-by: Emerson Knapp <eknapp@amazon.com>
Signed-off-by: Emerson Knapp <eknapp@amazon.com>
Signed-off-by: Emerson Knapp <eknapp@amazon.com>
Signed-off-by: Emerson Knapp <eknapp@amazon.com>
Signed-off-by: Emerson Knapp <eknapp@amazon.com>
Signed-off-by: Emerson Knapp <eknapp@amazon.com>
Signed-off-by: Emerson Knapp <eknapp@amazon.com>
3e1b0fe
to
e61bc6c
Compare
Closing in favor of ros2#316 |
Depends on ros2/rcl#408
Expose QoS event callbacks to the ROS2 Python client.