TaskTree: Introduce WorkflowPolicy::StopOnFinished

The policy is useful mainly in parallel mode.
It stops executing the Group when any task finishes.
It reports the task's result.

Change-Id: I7aa98365cdc4c1eb869ab419d42d0cc5438d43bf
Reviewed-by: <github-actions-qt-creator@cristianadam.eu>
Reviewed-by: hjk <hjk@qt.io>
This commit is contained in:
Jarek Kobus
2023-05-17 19:28:57 +02:00
parent 7bfc3197aa
commit 7501d7587f
3 changed files with 115 additions and 47 deletions

View File

@@ -114,6 +114,7 @@ Workflow stopOnError(WorkflowPolicy::StopOnError);
Workflow continueOnError(WorkflowPolicy::ContinueOnError); Workflow continueOnError(WorkflowPolicy::ContinueOnError);
Workflow stopOnDone(WorkflowPolicy::StopOnDone); Workflow stopOnDone(WorkflowPolicy::StopOnDone);
Workflow continueOnDone(WorkflowPolicy::ContinueOnDone); Workflow continueOnDone(WorkflowPolicy::ContinueOnDone);
Workflow stopOnFinished(WorkflowPolicy::StopOnFinished);
Workflow optional(WorkflowPolicy::Optional); Workflow optional(WorkflowPolicy::Optional);
void TaskItem::addChildren(const QList<TaskItem> &children) void TaskItem::addChildren(const QList<TaskItem> &children)
@@ -511,6 +512,10 @@ bool TaskContainer::RuntimeData::updateSuccessBit(bool success)
{ {
if (m_constData.m_workflowPolicy == WorkflowPolicy::Optional) if (m_constData.m_workflowPolicy == WorkflowPolicy::Optional)
return m_successBit; return m_successBit;
if (m_constData.m_workflowPolicy == WorkflowPolicy::StopOnFinished) {
m_successBit = success;
return m_successBit;
}
const bool donePolicy = m_constData.m_workflowPolicy == WorkflowPolicy::StopOnDone const bool donePolicy = m_constData.m_workflowPolicy == WorkflowPolicy::StopOnDone
|| m_constData.m_workflowPolicy == WorkflowPolicy::ContinueOnDone; || m_constData.m_workflowPolicy == WorkflowPolicy::ContinueOnDone;
@@ -595,8 +600,9 @@ TaskAction TaskContainer::childDone(bool success)
{ {
QTC_CHECK(isRunning()); QTC_CHECK(isRunning());
const int limit = m_runtimeData->currentLimit(); // Read before bumping m_doneCount and stop() const int limit = m_runtimeData->currentLimit(); // Read before bumping m_doneCount and stop()
const bool shouldStop = (m_constData.m_workflowPolicy == WorkflowPolicy::StopOnDone && success) const bool shouldStop = m_constData.m_workflowPolicy == WorkflowPolicy::StopOnFinished
|| (m_constData.m_workflowPolicy == WorkflowPolicy::StopOnError && !success); || (m_constData.m_workflowPolicy == WorkflowPolicy::StopOnDone && success)
|| (m_constData.m_workflowPolicy == WorkflowPolicy::StopOnError && !success);
if (shouldStop) if (shouldStop)
stop(); stop();
@@ -1146,15 +1152,15 @@ void TaskNode::invokeEndHandler(bool success)
\row \row
\li stopOnError \li stopOnError
\li Default. If a task finishes with an error, the group: \li Default. If a task finishes with an error, the group:
\list 1 \list 1
\li Stops the running tasks (if any - for example, in parallel \li Stops the running tasks (if any - for example, in parallel
mode). mode).
\li Skips executing tasks it has not started (for example, in the \li Skips executing tasks it has not started (for example, in the
sequential mode). sequential mode).
\li Immediately finishes with an error. \li Immediately finishes with an error.
\endlist \endlist
If all child tasks finish successfully or the group is empty, the group If all child tasks finish successfully or the group is empty, the group
finishes with success. finishes with success.
\row \row
\li continueOnError \li continueOnError
\li Similar to stopOnError, but in case any child finishes with \li Similar to stopOnError, but in case any child finishes with
@@ -1162,22 +1168,22 @@ void TaskNode::invokeEndHandler(bool success)
and the group reports an error afterwards, even when some other and the group reports an error afterwards, even when some other
tasks in group finished with success. tasks in group finished with success.
If a task finishes with an error, the group: If a task finishes with an error, the group:
\list 1 \list 1
\li Continues executing the tasks that are running or have not \li Continues executing the tasks that are running or have not
started yet. started yet.
\li Finishes with an error when all tasks finish. \li Finishes with an error when all tasks finish.
\endlist \endlist
If all tasks finish successfully or the group is empty, the group If all tasks finish successfully or the group is empty, the group
finishes with success. finishes with success.
\row \row
\li stopOnDone \li stopOnDone
\li If a task finishes with success, the group: \li If a task finishes with success, the group:
\list 1 \list 1
\li Stops running tasks and skips those that it has not started. \li Stops running tasks and skips those that it has not started.
\li Immediately finishes with success. \li Immediately finishes with success.
\endlist \endlist
If all tasks finish with an error or the group is empty, the group If all tasks finish with an error or the group is empty, the group
finishes with an error. finishes with an error.
\row \row
\li continueOnDone \li continueOnDone
\li Similar to stopOnDone, but in case any child finishes \li Similar to stopOnDone, but in case any child finishes
@@ -1185,13 +1191,21 @@ void TaskNode::invokeEndHandler(bool success)
and the group reports success afterwards, even when some other and the group reports success afterwards, even when some other
tasks in group finished with an error. tasks in group finished with an error.
If a task finishes with success, the group: If a task finishes with success, the group:
\list 1 \list 1
\li Continues executing the tasks that are running or have not \li Continues executing the tasks that are running or have not
started yet. started yet.
\li Finishes with success when all tasks finish. \li Finishes with success when all tasks finish.
\endlist \endlist
If all tasks finish with an error or the group is empty, the group If all tasks finish with an error or the group is empty, the group
finishes with an error. finishes with an error.
\row
\li stopOnFinished
\li The group starts as many tasks as it can. When a task finishes
the group stops and reports the task's result.
When the group is empty, it finishes immediately with success.
Useful only in parallel mode.
In sequential mode, only the first task is started, and when finished,
the group finishes too, so the other tasks are ignored.
\row \row
\li optional \li optional
\li The group executes all tasks and ignores their return state. If all \li The group executes all tasks and ignores their return state. If all

View File

@@ -93,19 +93,22 @@ private:
// WorkflowPolicy: // WorkflowPolicy:
// 1. When all children finished with done -> report done, otherwise: // 1. When all children finished with done -> report done, otherwise:
// a) Report error on first error and stop executing other children (including their subtree) // a) Report error on first error and stop executing other children (including their subtree).
// b) On first error - continue executing all children and report error afterwards // b) On first error - continue executing all children and report error afterwards.
// 2. When all children finished with error -> report error, otherwise: // 2. When all children finished with error -> report error, otherwise:
// a) Report done on first done and stop executing other children (including their subtree) // a) Report done on first done and stop executing other children (including their subtree).
// b) On first done - continue executing all children and report done afterwards // b) On first done - continue executing all children and report done afterwards.
// 3. Always run all children, ignore their result and report done afterwards // 3. Stops on first finished child. In sequential mode it will never run other children then the first one.
// Useful only in parallel mode.
// 4. Always run all children, ignore their result and report done afterwards.
enum class WorkflowPolicy { enum class WorkflowPolicy {
StopOnError, // 1a - Reports error on first child error, otherwise done (if all children were done) StopOnError, // 1a - Reports error on first child error, otherwise done (if all children were done).
ContinueOnError, // 1b - The same, but children execution continues. When no children it reports done. ContinueOnError, // 1b - The same, but children execution continues. Reports done when no children.
StopOnDone, // 2a - Reports done on first child done, otherwise error (if all children were error) StopOnDone, // 2a - Reports done on first child done, otherwise error (if all children were error).
ContinueOnDone, // 2b - The same, but children execution continues. When no children it reports done. (?) ContinueOnDone, // 2b - The same, but children execution continues. Reports error when no children.
Optional // 3 - Always reports done after all children finished StopOnFinished, // 3 - Stops on first finished child and report its result.
Optional // 4 - Reports done after all children finished.
}; };
enum class TaskAction enum class TaskAction
@@ -287,6 +290,7 @@ TASKING_EXPORT extern Workflow stopOnError;
TASKING_EXPORT extern Workflow continueOnError; TASKING_EXPORT extern Workflow continueOnError;
TASKING_EXPORT extern Workflow stopOnDone; TASKING_EXPORT extern Workflow stopOnDone;
TASKING_EXPORT extern Workflow continueOnDone; TASKING_EXPORT extern Workflow continueOnDone;
TASKING_EXPORT extern Workflow stopOnFinished;
TASKING_EXPORT extern Workflow optional; TASKING_EXPORT extern Workflow optional;
template <typename Task> template <typename Task>

View File

@@ -233,8 +233,9 @@ void tst_Tasking::testTree_data()
const auto setupFailingTask = [setupTaskHelper](int taskId) { const auto setupFailingTask = [setupTaskHelper](int taskId) {
return [=](TestTask &task) { setupTaskHelper(task, taskId, false); }; return [=](TestTask &task) { setupTaskHelper(task, taskId, false); };
}; };
const auto setupSleepingTask = [setupTaskHelper](int taskId, std::chrono::milliseconds sleep) { const auto setupSleepingTask = [setupTaskHelper](int taskId, bool success,
return [=](TestTask &task) { setupTaskHelper(task, taskId, true, sleep); }; std::chrono::milliseconds sleep) {
return [=](TestTask &task) { setupTaskHelper(task, taskId, success, sleep); };
}; };
const auto setupDynamicTask = [setupTaskHelper](int taskId, TaskAction action) { const auto setupDynamicTask = [setupTaskHelper](int taskId, TaskAction action) {
return [=](TestTask &task) { return [=](TestTask &task) {
@@ -680,6 +681,55 @@ void tst_Tasking::testTree_data()
QTest::newRow("ContinueOnDone") << TestData{storage, root, log, 3, OnDone::Success}; QTest::newRow("ContinueOnDone") << TestData{storage, root, log, 3, OnDone::Success};
} }
{
const Group root = constructSimpleSequence(stopOnFinished);
const Log log {
{1, Handler::Setup},
{1, Handler::Done},
{0, Handler::GroupDone}
};
QTest::newRow("StopOnFinished") << TestData{storage, root, log, 3, OnDone::Success};
}
{
const auto setupRoot = [=](bool firstSuccess, bool secondSuccess) {
return Group {
parallel,
stopOnFinished,
Storage(storage),
Test(setupSleepingTask(1, firstSuccess, 1000ms), logDone, logError),
Test(setupSleepingTask(2, secondSuccess, 5ms), logDone, logError),
OnGroupDone(groupDone(0)),
OnGroupError(groupError(0))
};
};
const Group root1 = setupRoot(true, true);
const Group root2 = setupRoot(true, false);
const Group root3 = setupRoot(false, true);
const Group root4 = setupRoot(false, false);
const Log success {
{1, Handler::Setup},
{2, Handler::Setup},
{2, Handler::Done},
{1, Handler::Error},
{0, Handler::GroupDone}
};
const Log failure {
{1, Handler::Setup},
{2, Handler::Setup},
{2, Handler::Error},
{1, Handler::Error},
{0, Handler::GroupError}
};
QTest::newRow("StopOnFinished1") << TestData{storage, root1, success, 2, OnDone::Success};
QTest::newRow("StopOnFinished2") << TestData{storage, root2, failure, 2, OnDone::Failure};
QTest::newRow("StopOnFinished3") << TestData{storage, root3, success, 2, OnDone::Success};
QTest::newRow("StopOnFinished4") << TestData{storage, root4, failure, 2, OnDone::Failure};
}
{ {
const Group root { const Group root {
Storage(storage), Storage(storage),
@@ -837,14 +887,14 @@ void tst_Tasking::testTree_data()
// Inside this test the task 2 should finish first, then synchonously: // Inside this test the task 2 should finish first, then synchonously:
// - task 3 should exit setup with error // - task 3 should exit setup with error
// - task 1 should be stopped as a consequence of error inside the group // - task 1 should be stopped as a consequence of the error inside the group
// - tasks 4 and 5 should be skipped // - tasks 4 and 5 should be skipped
const Group root2 { const Group root2 {
ParallelLimit(2), ParallelLimit(2),
Storage(storage), Storage(storage),
Group { Group {
OnGroupSetup(groupSetup(1)), OnGroupSetup(groupSetup(1)),
Test(setupSleepingTask(1, 10ms)) Test(setupSleepingTask(1, true, 10ms))
}, },
Group { Group {
OnGroupSetup(groupSetup(2)), OnGroupSetup(groupSetup(2)),
@@ -879,11 +929,11 @@ void tst_Tasking::testTree_data()
ParallelLimit(2), ParallelLimit(2),
Group { Group {
OnGroupSetup(groupSetup(1)), OnGroupSetup(groupSetup(1)),
Test(setupSleepingTask(1, 20ms)) Test(setupSleepingTask(1, true, 20ms))
}, },
Group { Group {
OnGroupSetup(groupSetup(2)), OnGroupSetup(groupSetup(2)),
Test(setupTask(2), [](const TestTask &) { QThread::msleep(10); }) Test(setupSleepingTask(2, true, 10ms))
}, },
Group { Group {
OnGroupSetup(groupSetup(3)), OnGroupSetup(groupSetup(3)),