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

Fix early preempt when planning #597

Merged
merged 6 commits into from
Jul 17, 2024

Conversation

captain-yoshi
Copy link
Contributor

We have a usecase in which we need to plan the task in another thread and sometimes preempt it. When preempting early, this does not work because the preempt flag is reseted inside the plan() method.

This fix makes the preempt flag changeable from the user. So for the first planning nothing changes. But users must reset the preempt flag before each subsequent planning (if the preempt method is used).


Even with this fix, preempting a task can take some time. The preempt flag is only checked at the start of the plan compute loop. #312 seems far away, but I think we could reduce the time it takes to preempt a task without too much changes ?

@codecov-commenter
Copy link

codecov-commenter commented Jul 16, 2024

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

Attention: Patch coverage is 41.17647% with 10 lines in your changes missing coverage. Please review.

Project coverage is 56.44%. Comparing base (6b0f2c8) to head (167a57b).
Report is 34 commits behind head on master.

Files Patch % Lines
core/test/test_container.cpp 30.77% 9 Missing ⚠️
core/src/task.cpp 75.00% 1 Missing ⚠️

❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #597      +/-   ##
==========================================
- Coverage   58.82%   56.44%   -2.37%     
==========================================
  Files          91      131      +40     
  Lines        8623    10684    +2061     
  Branches        0      951     +951     
==========================================
+ Hits         5072     6030     +958     
- Misses       3551     4607    +1056     
- Partials        0       47      +47     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link
Contributor

@rhaschke rhaschke left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When preempting early, this does not work because the preempt flag is reseted inside the plan() method.

I don't yet understand the race condition. Do you mean, preempt_requested_ may be set before it is initialized in plan() and then the request is lost?

Copy link
Contributor

@rhaschke rhaschke left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like that the user needs to remember to reset the preempt_requested_ flag.
What about resetting the flag when leaving Task::plan()?

moveit::core::MoveItErrorCode plan(size_t max_solutions = 0);
/// interrupt current planning (or execution)
void preempt();
/// interrupt current planning (or execution) or reset the preempt flag (false)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Execution cannot be preempted with this call.

Suggested change
/// interrupt current planning (or execution) or reset the preempt flag (false)
/// interrupt current planning or reset the preempt flag (false)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be more clean. We would need a try catch block and catch everything in the plan() to reset the value.

@captain-yoshi
Copy link
Contributor Author

When preempting early, this does not work because the preempt flag is reseted inside the plan() method.

I don't yet understand the race condition. Do you mean, preempt_requested_ may be set before it is initialized in plan() and then the request is lost?

Yes exactly.

@captain-yoshi
Copy link
Contributor Author

captain-yoshi commented Jul 16, 2024

What about resetting the flag when leaving Task::plan()?

@rhaschke This would work in most of the cases (see last commit). But consider the following case:

Thread + Sequence Method Info
T1 Task::plan() The plan finisihes and the flag is resetted.
Main Task::preempt() Flag is set to true;
Main Task::plan() This planning will not work as the preempted flag is set to true.

So I don't think it can be done it automatically...


EDIT Unless we always need to reset the task between 2 consecutive plan(), we could reset the preempt_requested_ in Task::reset(). This would always work.

@captain-yoshi captain-yoshi force-pushed the fix-early-preempt branch 4 times, most recently from 6bbd283 to 2b82592 Compare July 16, 2024 18:28
Copy link
Contributor

@rhaschke rhaschke left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious how you manager to call preempt() before plan() actually started its work.
I do see the theoretical possibility, but practically that shouldn't occur.

t.add(std::make_unique<GeneratorMockup>(PredefinedCosts::constant(0.0)));
t.add(std::make_unique<TimedForwardMockup>(timeout));

// preempt before any stage computation is done
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// preempt before any stage computation is done
// preempt before preempt_request_ is reset in plan()


// preempt before any stage computation is done
{
std::thread thread{ [&ec, &t] { ec = t.plan(1); } };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about introducing a small sleep before t.plan() in order to emphasize the issue even more?

Comment on lines 267 to 268
pimpl()->preempt_requested_ = false;
throw; // rethrow the original exception
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The try-catch-block is the wrong tool to cleanup before leaving the function scope: it is only considered if an exception is thrown, not when leaving the function via return. Please revert this.
What you need is a scope guard:
https://stackoverflow.com/questions/50182244/simple-way-to-execute-code-at-the-end-of-function-scope

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would using this external library be acceptable ? At a first glance it seems well tested and easy to use.

A custom CMakeLists.txt is used to create the scope_guard libray. The
original cmake libray file is only used to setup tests.
See ricab/scope_guard#3.
Remove the initialization of preemt_requested_ in Task::plan(). Make preempted request atomic.
Added method to reset the preempt request.
@captain-yoshi
Copy link
Contributor Author

I'm curious how you manager to call preempt() before plan() actually started its work.

We started using BehaviorTree.CPP to create MTC tasks. I was adding a test to test the preempting of a BT node when planning. Here is the sample.

<root BTCPP_format="4" >
  <BehaviorTree ID="MainTree">
    <Sequence>
      <Script code="test := true"/>
      <ReactiveSequence name="root">
        <ForceFailure _skipIf="test">
          <Script code=" result:='error' " />
        </ForceFailure>

        <Script code=" result:='error' " _onSuccess="test = false" />
        <PlanMTCTask task="{mtc_task}" max_solutions="1000" />

      </ReactiveSequence>
    </Sequence>
  </BehaviorTree>
</root>

So planning is first started in another thread. Then the main thread will check if the BT node PlanMTCTask is done. After that the ReactiveSequence will halt all children because of ForceFailure triggering on the second pass.

@captain-yoshi
Copy link
Contributor Author

@rhaschke Ready for another review with all changes requested.

This adds an external dependency to the scope_guard library.

@rhaschke
Copy link
Contributor

If you agree to my simplification, this is ready to merge.

@captain-yoshi
Copy link
Contributor Author

LGTM and thanks for the review.

@rhaschke rhaschke merged commit cd28bdc into moveit:master Jul 17, 2024
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants