Skip to content

EventsCBGExecutor treats reentrant callback groups like they're mutually exclusive #3175

Description

@armaho

Generated by Generative AI

No

Operating System:

Linux pi 7.0.0-1011-raspi-realtime #11-Ubuntu SMP PREEMPT_RT Tue May 26 15:20:46 UTC 2026 aarch64 GNU/Linux

ROS version or commit hash:

rolling

RMW implementation (if applicable):

No response

RMW Configuration (if applicable):

No response

Client library (if applicable):

No response

'ros2 doctor --report' output

No response

Steps to reproduce issue

I used this piece of code. I also used yaets for tracing the callbacks.

#include "rclcpp/rclcpp.hpp"
#include <fstream>

// #include "yaets/tracing.hpp"

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/int32.hpp"

using namespace std::chrono_literals;
using std::placeholders::_1;


// yaets::TraceSession session("session1.log");

class ProducerNode : public rclcpp::Node
{
public:
  ProducerNode() : Node("producer_node")
  {
    pub_1_ = create_publisher<std_msgs::msg::Int32>("topic_1", 100);
    pub_2_ = create_publisher<std_msgs::msg::Int32>("topic_2", 100);
    timer_ = create_wall_timer(1ms, std::bind(&ProducerNode::timer_callback, this));
  }

  void timer_callback()
  {
    // TRACE_EVENT(session);
    message_.data += 1;
    pub_1_->publish(message_);
    message_.data += 1;
    pub_2_->publish(message_);
  }

private:
  rclcpp::Publisher<std_msgs::msg::Int32>::SharedPtr pub_1_, pub_2_;
  rclcpp::TimerBase::SharedPtr timer_;
  std_msgs::msg::Int32 message_;
};

class ConsumerNode : public rclcpp::Node
{
public:
  ConsumerNode() : Node("consumer_node")
  {
    auto cg = this->create_callback_group(rclcpp::CallbackGroupType::Reentrant);

    rclcpp::SubscriptionOptions opt;
    opt.callback_group = cg;

    sub_2_ = create_subscription<std_msgs::msg::Int32>(
      "topic_2", 100, std::bind(&ConsumerNode::cb_2, this, _1), opt);
    sub_1_ = create_subscription<std_msgs::msg::Int32>(
      "topic_1", 100, std::bind(&ConsumerNode::cb_1, this, _1), opt);
 
    timer_ = create_wall_timer(10ms, std::bind(&ConsumerNode::timer_callback, this));
  }

  void cb_1(const std_msgs::msg::Int32::SharedPtr msg)
  {
    // TRACE_EVENT(session);

    waste_time(500us);
  }

  void cb_2(const std_msgs::msg::Int32::SharedPtr msg)
  {
    // TRACE_EVENT(session);

    waste_time(500us);
  }

  void timer_callback()
  {
    // TRACE_EVENT(session);

    waste_time(5ms);
  }

  void waste_time(const rclcpp::Duration & duration)
  {
    auto start = now();
    while (now() - start < duration);
  }

private:
  rclcpp::Subscription<std_msgs::msg::Int32>::SharedPtr sub_1_;
  rclcpp::Subscription<std_msgs::msg::Int32>::SharedPtr sub_2_;
  rclcpp::TimerBase::SharedPtr timer_;
};

int main(int argc, char * argv[])
{
  rclcpp::init(argc, argv);

  auto node_pub = std::make_shared<ProducerNode>();
  auto node_sub = std::make_shared<ConsumerNode>();

  // rclcpp::executors::SingleThreadedExecutor executor;
  // rclcpp::executors::MultiThreadedExecutor executor(rclcpp::ExecutorOptions(), 8);
  rclcpp::executors::EventsCBGExecutor executor(rclcpp::ExecutorOptions(), 8);

  executor.add_node(node_pub);
  executor.add_node(node_sub);

  executor.spin();

  rclcpp::shutdown();
  return 0;
}

Expected behavior

As you can see, sub_1_ and sub_2_ are part of the same reentrant callback group. So, like what happens with MultiThreadedExecutor they should be able to execute simultaneously:

Image

Actual behavior

Under EventsCBGExecutor, These two callbacks are executed sequentially:

Image

Additional information

I might be completely wrong here. Sorry if I am :)

I think that the problem comes from the CallbackGroupHandle structure. It has a not_ready field:

// will be set if cbg is mutual exclusive and something is executing
bool not_ready = false;

Based on the code, it appears that this field will be set to true regardless of the actual type of the underlying callback group. It happens by calling mark_as_executing here:

std::optional<CBGScheduler::ExecutableEntity> FirstInFirstOutCallbackGroupHandle::
get_next_ready_entity()
{
  std::lock_guard l(ready_mutex);

  while(!ready_entities.empty()) {
    auto & first = ready_entities.front();

    std::function<void()> exec_fun = first.get_execute_function();
    ready_entities.pop_front();
    if(!exec_fun) {
      // was deleted, or in case of timer was canceled
      continue;
    }

    mark_as_executing();

    return CBGScheduler::ExecutableEntity{exec_fun, this};
  }

  mark_as_skipped();

  return std::nullopt;
}

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions