-
Notifications
You must be signed in to change notification settings - Fork 83
scripting: run functions asynchronously through the message API #156
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
scripting: run functions asynchronously through the message API #156
Conversation
|
Having the command now run asynchronously and "immediately" is a pretty significant change in semantics. I'm pretty sure that our systems would react poorly to such a change, and I imagine that this might likely affect other's systems also. |
90bf872 to
8bfda50
Compare
|
More thoughts? I do not see a better alternative to fix issue #150 at the moment. What would be possible is to execute functions asynchronously (without the need for an explicit trigger call) if the activity is non-periodic and synchronously for periodic activities. But it should be noted that the two cases cannot always be distinguished clearly, e.g. for the SlaveActivity or the FileDescriptorActivity. |
…ocos-toolchain#150) Instead of queueing a function in the function queue of the executing engine the new implementation of RTT::scripting::CallFunction implements a DisposableInterface and enqueues itself in the message queue which is processed asynchronously. This way the engine does not have to wait for a timeout or trigger and execute as soon as possible. The scripting_test is modified so that it would block indefinitely for the same reason as for the example script in orocos-toolchain#150 (without SequentialActivity). Signed-off-by: Johannes Meyer <[email protected]>
8bfda50 to
32bdc3c
Compare
Signed-off-by: Johannes Meyer <[email protected]>
… as a function to the caller Signed-off-by: Johannes Meyer <[email protected]>
scripting: enqueue called functions in the function queue after a yield
…ssMessages() Signed-off-by: Johannes Meyer <[email protected]>
A yield statement within a function does not have a well-defined meaning if the function was called from the same thread that is supposed to execute it, especially if it is nested within another function. The ExecutionEngine::waitAndProcessFunctions() method, that was called in this case, processed all functions running in the same engine again, even if not related to the ongoing call, but did not process operations or invoke updateHook() before it returns. This is totally unexpected behavior because - it's different from what a yield statement would do in programs, functions being sent or if the function is called from a different thread - other waiting yields are unblocked and state machines are stepped again within the same update cycle because of the intermingled calls to ExecutionEngine::processFunctions(). With this patch a yield statement becomes almost a no-op in this case and only processes the message queue before it returns, which handles pending operation calls. Other than before the patch in 32bdc3c, that made all function calls execute immediately without the need to wait for the next update cycle, it is not required anymore to process *all* functions again while waiting for the completion of the current one. ExecutionEngine::processFunctions() is called exactly once per update step. Signed-off-by: Johannes Meyer <[email protected]>
… robust The previous implementation of OperationCallerInterface::isSelf() and CallFunction::execute() did only check whether the thread returned by engine->getActivity()->thread() (same as engine->getThread()) is the same as the caller. For the case the ExecutionEngine runs in a SlaveActivity operations are processed by the thread of the master, which is not necessarily the same as the thread returned by getActivity()->thread(). This patch adds a reimplemented variant of getThread() and a new method isSelf() to the ExecutionEngine interface, where isSelf() returns true if and only if waitForMessages() would also process messages while waiting because it thinks that we are waiting for ourselves. Signed-off-by: Johannes Meyer <[email protected]>
|
The new patches added last week are based on the work in meyerj#6 and #206. They do not change the essence of this PR, namely that calling scripting functions gets closer to the behavior of calling OwnThread operations and execute in the trigger step of the execution engine instead of waiting for a full update step. The problematic cases are when other functions are called from the body or functions contain From meyerj#6:Additional patch for #156 that enqueues the call in the function queue if the first execution as a message/operation returns without finishing the function (yield). [...] From the commit message 611f3d1:A
With this patch a yield statement becomes almost a no-op in this case and only processes the message queue before it returns, which handles pending operation calls. Other than before the patch in 32bdc3c, that made all function calls execute immediately without the need to wait for the next update cycle, it is not required anymore to process all functions again while waiting for the completion of the current one. a5a6c95 and b2206bd are optimizations and only make the check for calling the same or another engine more explicit. Example## function_test.cpp
RTT::Attribute<int> i;
void printI() {
log() << " i = " << i << endlog();
}
void print(const std::string &text) {
log() << text << endlog();
}
void updateHook() {
log() << "updateHook()" << endlog();
}
## function_test.ops:
program backgroundTask {
while(true) {
print("backgroundTask triggered");
yield;
}
}
export void func1(void) {
print("func1()");
}
export void func2(void) {
print("func2()");
i = i + 1;
printI(i);
yield;
i = i + 1;
printI(i);
}
export void func3(void) {
print("func3()");
func2();
}
backgroundTask.start();
start();
func1();
func2();
func3();
## deploy.ops
loadComponent("test", "FunctionTest");
test.setPeriod(1.0);
# or test.setPeriod(0.0);
test.scripting.runScript("function_test.ops");Periodic activitymaster/toolchain-2.8/toolchain-2.9 without this patch: toolchain-2.9 with this patch: Non-periodic activityIn case the component would be non-periodic: master/toolchain-2.8 without this patch: toolchain-2.9 without this patch (as reported in #150): toolchain-2.9 with this patch: For the last case: Should calling a function that yields imply that the executing component is triggered repeatedly (full update steps) until the function is done? That would resolve the situation but might lead to unexpected executions of |
tests/scripting_test.cpp
Outdated
| BOOST_CHECK( tc->provides("scripting")->hasMember("func1")); | ||
|
|
||
| // define a function that calls func1, yields and calls func1 again: | ||
| statements = "void func2(void) { test.printNumber(\"[ENTER func2()] CycleCounter = \", CycleCounter); func1(); yield; func1(); test.printNumber(\"[EXIT func2()] CycleCounter = \", CycleCounter); }\n"; |
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 would be nice to format this for readability's sake.
kevindm
left a comment
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 looks like it is performing as described.
One remark: the current behaviour of 'yield' in a called function (ie. func3 calling func2 above) feels counter-intuitive. What happens when the following gets executed?
void wait() {
while (some_condition_that_can_only_become_true_async()) {
yield;
}
}
void func() {
print( "initiating wait.\n" );
wait();
print( "done waiting.\n" );
}
- renamed fooDone() to checkIfDoneOrYielded() - separate private helper function checkIfDone() to avoid duplicate code in CallFunction::execute() - split code path for synchronous and asynchronous function calls The throw false statement in case of errors has been moved to checkIfDone(). Signed-off-by: Johannes Meyer <[email protected]>
The flag is actually not required because we never return from execute() before the function was either successfully executed or ended up in error state. CallFunction::valid() should still return true after execute() returned, so it must not be cleared in executeAndDispose() if ProgramInterface::execute() returned false and the function is either done or in error. Signed-off-by: Johannes Meyer <[email protected]>
… code over multiple lines ...for improved readability. The check of the parser result now uses BOOST_REQUIRE() to abort the test case if parsing failed. Follow-up checks would fail anyway if the parser failed. Signed-off-by: Johannes Meyer <[email protected]>
…ets before the else clause Signed-off-by: Johannes Meyer <[email protected]>
|
Thanks, @kevindm, for your review. I think I addressed most of your comments. About your example: You are right and the script might block and busy-wait depending on what exactly The behavior of that example before this patch and in all versions of RTT 2.x was also ill-defined, because the To make another point: What should be expected from // Orocos scripting
export void wait() {
while (some_condition_that_can_only_become_true_async()) {
yield;
}
}
// C++
void func() {
log() << "initiating wait." << endlog();
wait();
log() << "done waiting." << endlog();
}if the scripting function Note that |
|
We could apply the same method as in cab8632 also to other unit tests to improve readability of scripting snippets. |
|
|
||
| // We use a sequential activity in order to force execution on trigger(). | ||
| tc->stop(); | ||
| BOOST_CHECK( tc->setActivity( new SequentialActivity() ) ); |
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.
All test cases in scripting_test.cpp still pass without switching to a true multi-threaded test and using the default Activity, but some problematic effects of nested function calls and yield statements would be hidden if they are executed sequentially by the main thread.
|
We could add a warning log output if a called function is yielding and the yield statement is ignored, because that's probably undesired behavior in almost all cases. Same is true for yield statements in scripts outside of functions or programs (cf. ScriptParser.cpp:92). As a side note: A |
…d would block its own thread Signed-off-by: Johannes Meyer <[email protected]>
…n-2.9-fix-150 into rdt-toolchain-2.9 Only three commits from the upstream PR have been picked, not the actual change related to function call execution semantics. b2206bd adds a isSelf() method to ExeutionEngine which is aware of master/slave relationships and which decides whether called functions are inlined (executed immediately by the calling thread to avoid dead-locks) or enqueued. The patch only makes a difference in cases where ExecutionEngine::setMaster() would be called explicitly and not only from ExecutionEngine::setActivity() if the new activity is an instance of SlaveActivity. cab8632 reformats the scripting_test.cpp and makes use of preprocessor stringification to improve readability of script code. With 4e3da36 Orocos scripting accepts a semicolon between the statement in the if clause and the else keyword, similar to C/C++ syntax rules, which before triggered a parsing error.
…n-2.9-fix-150 into rdt-toolchain-2.9 Only three commits from the upstream PR have been picked, not the actual change related to function call execution semantics. b2206bd adds a isSelf() method to ExeutionEngine which is aware of master/slave relationships and which decides whether called functions are inlined (executed immediately by the calling thread to avoid dead-locks) or enqueued. The patch only makes a difference in cases where ExecutionEngine::setMaster() would be called explicitly and not only from ExecutionEngine::setActivity() if the new activity is an instance of SlaveActivity. cab8632 reformats the scripting_test.cpp and makes use of preprocessor stringification to improve readability of script code. With 4e3da36 Orocos scripting accepts a semicolon between the statement in the if clause and the else keyword, similar to C/C++ syntax rules, which before triggered a parsing error.
scripting: run functions asynchronously through the message API
This is a better fix for #150 compared to the first proposal psoetens#7, which was too intrusive. It is based on @psoetens's
master-update-hook-vs-callback-queuebranch from #91.Whenever a scripting function (or any other instance of
ExecutableInterface) is called, the previous implementation added it to the runner's engine's function queue. With #91 this function queue is only processed if the component is triggered explicitly by either a timeout (if the component is periodic), an I/O event (in case it is running with aFileDescriptorActivity) or an explicittrigger()orupdate()call on the task. See the original PR for details.With patch 32bdc3c the
RTT::scripting::CallFunctionimplements theDisposableInterfaceand enqueues itself in the message queue of the executing engine instead of in the function queue. The message queue is processed asynchronously and does not wait for an explicit timeout or trigger event.