// Copyright (C) 2024 Jarek Kobus // Copyright (C) 2024 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #ifndef TASKING_TASKTREE_H #define TASKING_TASKTREE_H // // W A R N I N G // ------------- // // This file is not part of the Qt API. It exists purely as an // implementation detail. This header file may change from version to // version without notice, or even be removed. // // We mean it. // #include "tasking_global.h" #include #include #include QT_BEGIN_NAMESPACE template class QFuture; namespace Tasking { Q_NAMESPACE // WorkflowPolicy: // 1. When all children finished with success -> report success, otherwise: // 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. // 2. When all children finished with error -> report error, otherwise: // a) Report success on first success and stop executing other children (including their subtree). // b) On first success - continue executing all children and report success 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, let them finish, ignore their results and report success afterwards. // 5. Always run all children, let them finish, ignore their results and report error afterwards. enum class WorkflowPolicy { StopOnError, // 1a - Reports error on first child error, otherwise success (if all children were success). ContinueOnError, // 1b - The same, but children execution continues. Reports success when no children. StopOnSuccess, // 2a - Reports success on first child success, otherwise error (if all children were error). ContinueOnSuccess, // 2b - The same, but children execution continues. Reports error when no children. StopOnSuccessOrError, // 3 - Stops on first finished child and report its result. FinishAllAndSuccess, // 4 - Reports success after all children finished. FinishAllAndError // 5 - Reports error after all children finished. }; Q_ENUM_NS(WorkflowPolicy) enum class SetupResult { Continue, StopWithSuccess, StopWithError }; Q_ENUM_NS(SetupResult) enum class DoneResult { Success, Error }; Q_ENUM_NS(DoneResult) enum class DoneWith { Success, Error, Cancel }; Q_ENUM_NS(DoneWith) enum class CallDoneIf { SuccessOrError, Success, Error }; Q_ENUM_NS(CallDoneIf) TASKING_EXPORT DoneResult toDoneResult(bool success); class LoopData; class StorageData; class TaskTreePrivate; class TASKING_EXPORT TaskInterface : public QObject { Q_OBJECT Q_SIGNALS: void done(DoneResult result); private: template friend class TaskAdapter; friend class TaskTreePrivate; TaskInterface() = default; #ifdef Q_QDOC protected: #endif virtual void start() = 0; }; class TASKING_EXPORT Loop { public: using Condition = std::function; // Takes iteration, called prior to each iteration. using ValueGetter = std::function; // Takes iteration, returns ptr to ref. int iteration() const; protected: Loop(); // LoopForever Loop(int count, const ValueGetter &valueGetter = {}); // LoopRepeat, LoopList Loop(const Condition &condition); // LoopUntil const void *valuePtr() const; private: friend class ExecutionContextActivator; friend class TaskTreePrivate; std::shared_ptr m_loopData; }; class TASKING_EXPORT LoopForever final : public Loop { public: LoopForever() : Loop() {} }; class TASKING_EXPORT LoopRepeat final : public Loop { public: LoopRepeat(int count) : Loop(count) {} }; class TASKING_EXPORT LoopUntil final : public Loop { public: LoopUntil(const Condition &condition) : Loop(condition) {} }; template class LoopList final : public Loop { public: LoopList(const QList &list) : Loop(list.size(), [list](int i) { return &list.at(i); }) {} const T *operator->() const { return static_cast(valuePtr()); } const T &operator*() const { return *static_cast(valuePtr()); } }; class TASKING_EXPORT StorageBase { private: using StorageConstructor = std::function; using StorageDestructor = std::function; using StorageHandler = std::function; StorageBase(const StorageConstructor &ctor, const StorageDestructor &dtor); void *activeStorageVoid() const; friend bool operator==(const StorageBase &first, const StorageBase &second) { return first.m_storageData == second.m_storageData; } friend bool operator!=(const StorageBase &first, const StorageBase &second) { return first.m_storageData != second.m_storageData; } friend size_t qHash(const StorageBase &storage, uint seed = 0) { return size_t(storage.m_storageData.get()) ^ seed; } std::shared_ptr m_storageData; template friend class Storage; friend class ExecutionContextActivator; friend class StorageData; friend class RuntimeContainer; friend class TaskTree; friend class TaskTreePrivate; }; template class Storage final : public StorageBase { public: Storage() : StorageBase(Storage::ctor(), Storage::dtor()) {} StorageStruct &operator*() const noexcept { return *activeStorage(); } StorageStruct *operator->() const noexcept { return activeStorage(); } StorageStruct *activeStorage() const { return static_cast(activeStorageVoid()); } private: static StorageConstructor ctor() { return [] { return new StorageStruct(); }; } static StorageDestructor dtor() { return [](void *storage) { delete static_cast(storage); }; } }; class TASKING_EXPORT GroupItem { public: // Called when group entered, after group's storages are created using GroupSetupHandler = std::function; // Called when group done, before group's storages are deleted using GroupDoneHandler = std::function; template GroupItem(const Storage &storage) : m_type(Type::Storage) , m_storageList{storage} {} // TODO: Add tests. GroupItem(const QList &children) : m_type(Type::List) { addChildren(children); } GroupItem(std::initializer_list children) : m_type(Type::List) { addChildren(children); } protected: GroupItem(const Loop &loop) : GroupItem(GroupData{{}, {}, {}, loop}) {} // Internal, provided by CustomTask using InterfaceCreateHandler = std::function; // Called prior to task start, just after createHandler using InterfaceSetupHandler = std::function; // Called on task done, just before deleteLater using InterfaceDoneHandler = std::function; struct TaskHandler { InterfaceCreateHandler m_createHandler; InterfaceSetupHandler m_setupHandler = {}; InterfaceDoneHandler m_doneHandler = {}; CallDoneIf m_callDoneIf = CallDoneIf::SuccessOrError; }; struct GroupHandler { GroupSetupHandler m_setupHandler; GroupDoneHandler m_doneHandler = {}; CallDoneIf m_callDoneIf = CallDoneIf::SuccessOrError; }; struct GroupData { GroupHandler m_groupHandler = {}; std::optional m_parallelLimit = {}; std::optional m_workflowPolicy = {}; std::optional m_loop = {}; }; enum class Type { List, Group, GroupData, Storage, TaskHandler }; GroupItem() = default; GroupItem(Type type) : m_type(type) { } GroupItem(const GroupData &data) : m_type(Type::GroupData) , m_groupData(data) {} GroupItem(const TaskHandler &handler) : m_type(Type::TaskHandler) , m_taskHandler(handler) {} void addChildren(const QList &children); static GroupItem groupHandler(const GroupHandler &handler) { return GroupItem({handler}); } // Checks if Function may be invoked with Args and if Function's return type is Result. template > static constexpr bool isInvocable() { // Note, that std::is_invocable_r_v doesn't check Result type properly. if constexpr (std::is_invocable_r_v) return std::is_same_v>; return false; } private: friend class ContainerNode; friend class For; friend class TaskNode; friend class TaskTreePrivate; friend class ParallelLimitFunctor; friend class WorkflowPolicyFunctor; Type m_type = Type::Group; QList m_children; GroupData m_groupData; QList m_storageList; TaskHandler m_taskHandler; }; class TASKING_EXPORT ExecutableItem : public GroupItem { public: ExecutableItem withTimeout(std::chrono::milliseconds timeout, const std::function &handler = {}) const; ExecutableItem withLog(const QString &logName) const; template ExecutableItem withCancel(SenderSignalPairGetter &&getter) const { const auto connectWrapper = [getter](QObject *guard, const std::function &trigger) { const auto senderSignalPair = getter(); QObject::connect(senderSignalPair.first, senderSignalPair.second, guard, [trigger] { trigger(); }, static_cast(Qt::QueuedConnection | Qt::SingleShotConnection)); }; return withCancelImpl(connectWrapper); } protected: ExecutableItem() = default; ExecutableItem(const TaskHandler &handler) : GroupItem(handler) {} private: TASKING_EXPORT friend ExecutableItem operator!(const ExecutableItem &item); TASKING_EXPORT friend ExecutableItem operator&&(const ExecutableItem &first, const ExecutableItem &second); TASKING_EXPORT friend ExecutableItem operator||(const ExecutableItem &first, const ExecutableItem &second); TASKING_EXPORT friend ExecutableItem operator&&(const ExecutableItem &item, DoneResult result); TASKING_EXPORT friend ExecutableItem operator||(const ExecutableItem &item, DoneResult result); ExecutableItem withCancelImpl( const std::function &)> &connectWrapper) const; }; class TASKING_EXPORT Group : public ExecutableItem { public: Group(const QList &children) { addChildren(children); } Group(std::initializer_list children) { addChildren(children); } // GroupData related: template static GroupItem onGroupSetup(Handler &&handler) { return groupHandler({wrapGroupSetup(std::forward(handler))}); } template static GroupItem onGroupDone(Handler &&handler, CallDoneIf callDoneIf = CallDoneIf::SuccessOrError) { return groupHandler({{}, wrapGroupDone(std::forward(handler)), callDoneIf}); } private: template static GroupSetupHandler wrapGroupSetup(Handler &&handler) { // R, V stands for: Setup[R]esult, [V]oid static constexpr bool isR = isInvocable(); static constexpr bool isV = isInvocable(); static_assert(isR || isV, "Group setup handler needs to take no arguments and has to return void or SetupResult. " "The passed handler doesn't fulfill these requirements."); return [handler] { if constexpr (isR) return std::invoke(handler); std::invoke(handler); return SetupResult::Continue; }; } template static GroupDoneHandler wrapGroupDone(Handler &&handler) { static constexpr bool isDoneResultType = std::is_same_v; // R, B, V, D stands for: Done[R]esult, [B]ool, [V]oid, [D]oneWith static constexpr bool isRD = isInvocable(); static constexpr bool isR = isInvocable(); static constexpr bool isBD = isInvocable(); static constexpr bool isB = isInvocable(); static constexpr bool isVD = isInvocable(); static constexpr bool isV = isInvocable(); static_assert(isDoneResultType || isRD || isR || isBD || isB || isVD || isV, "Group done handler needs to take (DoneWith) or (void) as an argument and has to " "return void, bool or DoneResult. Alternatively, it may be of DoneResult type. " "The passed handler doesn't fulfill these requirements."); return [handler](DoneWith result) { if constexpr (isDoneResultType) return handler; if constexpr (isRD) return std::invoke(handler, result); if constexpr (isR) return std::invoke(handler); if constexpr (isBD) return toDoneResult(std::invoke(handler, result)); if constexpr (isB) return toDoneResult(std::invoke(handler)); if constexpr (isVD) std::invoke(handler, result); else if constexpr (isV) std::invoke(handler); return toDoneResult(result == DoneWith::Success); }; } }; template static GroupItem onGroupSetup(Handler &&handler) { return Group::onGroupSetup(std::forward(handler)); } template static GroupItem onGroupDone(Handler &&handler, CallDoneIf callDoneIf = CallDoneIf::SuccessOrError) { return Group::onGroupDone(std::forward(handler), callDoneIf); } class TASKING_EXPORT ParallelLimitFunctor { public: // Default: 1 (sequential). 0 means unlimited (parallel). GroupItem operator()(int limit) const; }; class TASKING_EXPORT WorkflowPolicyFunctor { public: // Default: WorkflowPolicy::StopOnError. GroupItem operator()(WorkflowPolicy policy) const; }; TASKING_EXPORT extern const ParallelLimitFunctor parallelLimit; TASKING_EXPORT extern const WorkflowPolicyFunctor workflowPolicy; TASKING_EXPORT extern const GroupItem sequential; TASKING_EXPORT extern const GroupItem parallel; TASKING_EXPORT extern const GroupItem parallelIdealThreadCountLimit; TASKING_EXPORT extern const GroupItem stopOnError; TASKING_EXPORT extern const GroupItem continueOnError; TASKING_EXPORT extern const GroupItem stopOnSuccess; TASKING_EXPORT extern const GroupItem continueOnSuccess; TASKING_EXPORT extern const GroupItem stopOnSuccessOrError; TASKING_EXPORT extern const GroupItem finishAllAndSuccess; TASKING_EXPORT extern const GroupItem finishAllAndError; TASKING_EXPORT extern const GroupItem nullItem; TASKING_EXPORT extern const ExecutableItem successItem; TASKING_EXPORT extern const ExecutableItem errorItem; class TASKING_EXPORT For : public Group { public: template For(const Loop &loop, const Args &...args) : Group(withLoop(loop, args...)) { } protected: For(const Loop &loop, const QList &children) : Group({loop, children}) {} For(const Loop &loop, std::initializer_list children) : Group({loop, children}) {} private: template QList withLoop(const Loop &loop, const Args &...args) { QList children{GroupItem(loop)}; appendChildren(std::make_tuple(args...), &children); return children; } template void appendChildren(const Tuple &tuple, QList *children) { constexpr auto TupleSize = std::tuple_size_v; if constexpr (TupleSize > 0) { // static_assert(workflowPolicyCount() <= 1, "Too many workflow policies in one group."); children->append(std::get(tuple)); if constexpr (N + 1 < TupleSize) appendChildren(tuple, children); } } }; class TASKING_EXPORT Forever final : public For { public: Forever(const QList &children) : For(LoopForever(), children) {} Forever(std::initializer_list children) : For(LoopForever(), children) {} }; // Synchronous invocation. Similarly to Group - isn't counted as a task inside taskCount() class TASKING_EXPORT Sync final : public ExecutableItem { public: template Sync(Handler &&handler) { addChildren({ onGroupDone(wrapHandler(std::forward(handler))) }); } private: template static auto wrapHandler(Handler &&handler) { // R, B, V stands for: Done[R]esult, [B]ool, [V]oid static constexpr bool isR = isInvocable(); static constexpr bool isB = isInvocable(); static constexpr bool isV = isInvocable(); static_assert(isR || isB || isV, "Sync handler needs to take no arguments and has to return void, bool or DoneResult. " "The passed handler doesn't fulfill these requirements."); return handler; } }; template > class TaskAdapter : public TaskInterface { protected: TaskAdapter() : m_task(new Task) {} Task *task() { return m_task.get(); } const Task *task() const { return m_task.get(); } private: using TaskType = Task; using DeleterType = Deleter; template friend class CustomTask; std::unique_ptr m_task; }; template class CustomTask final : public ExecutableItem { public: using Task = typename Adapter::TaskType; using Deleter = typename Adapter::DeleterType; static_assert(std::is_base_of_v, Adapter>, "The Adapter type for the CustomTask needs to be derived from " "TaskAdapter."); using TaskSetupHandler = std::function; using TaskDoneHandler = std::function; template CustomTask(SetupHandler &&setup = TaskSetupHandler(), DoneHandler &&done = TaskDoneHandler(), CallDoneIf callDoneIf = CallDoneIf::SuccessOrError) : ExecutableItem({&createAdapter, wrapSetup(std::forward(setup)), wrapDone(std::forward(done)), callDoneIf}) {} private: static Adapter *createAdapter() { return new Adapter; } template static InterfaceSetupHandler wrapSetup(Handler &&handler) { if constexpr (std::is_same_v) return {}; // When user passed {} for the setup handler. // R, V stands for: Setup[R]esult, [V]oid static constexpr bool isR = isInvocable(); static constexpr bool isV = isInvocable(); static_assert(isR || isV, "Task setup handler needs to take (Task &) as an argument and has to return void or " "SetupResult. The passed handler doesn't fulfill these requirements."); return [handler](TaskInterface &taskInterface) { Adapter &adapter = static_cast(taskInterface); if constexpr (isR) return std::invoke(handler, *adapter.task()); std::invoke(handler, *adapter.task()); return SetupResult::Continue; }; } template static InterfaceDoneHandler wrapDone(Handler &&handler) { if constexpr (std::is_same_v) return {}; // User passed {} for the done handler. static constexpr bool isDoneResultType = std::is_same_v; // R, B, V, T, D stands for: Done[R]esult, [B]ool, [V]oid, [T]ask, [D]oneWith static constexpr bool isRTD = isInvocable(); static constexpr bool isRT = isInvocable(); static constexpr bool isRD = isInvocable(); static constexpr bool isR = isInvocable(); static constexpr bool isBTD = isInvocable(); static constexpr bool isBT = isInvocable(); static constexpr bool isBD = isInvocable(); static constexpr bool isB = isInvocable(); static constexpr bool isVTD = isInvocable(); static constexpr bool isVT = isInvocable(); static constexpr bool isVD = isInvocable(); static constexpr bool isV = isInvocable(); static_assert(isDoneResultType || isRTD || isRT || isRD || isR || isBTD || isBT || isBD || isB || isVTD || isVT || isVD || isV, "Task done handler needs to take (const Task &, DoneWith), (const Task &), " "(DoneWith) or (void) as arguments and has to return void, bool or DoneResult. " "Alternatively, it may be of DoneResult type. " "The passed handler doesn't fulfill these requirements."); return [handler](const TaskInterface &taskInterface, DoneWith result) { if constexpr (isDoneResultType) return handler; const Adapter &adapter = static_cast(taskInterface); if constexpr (isRTD) return std::invoke(handler, *adapter.task(), result); if constexpr (isRT) return std::invoke(handler, *adapter.task()); if constexpr (isRD) return std::invoke(handler, result); if constexpr (isR) return std::invoke(handler); if constexpr (isBTD) return toDoneResult(std::invoke(handler, *adapter.task(), result)); if constexpr (isBT) return toDoneResult(std::invoke(handler, *adapter.task())); if constexpr (isBD) return toDoneResult(std::invoke(handler, result)); if constexpr (isB) return toDoneResult(std::invoke(handler)); if constexpr (isVTD) std::invoke(handler, *adapter.task(), result); else if constexpr (isVT) std::invoke(handler, *adapter.task()); else if constexpr (isVD) std::invoke(handler, result); else if constexpr (isV) std::invoke(handler); return toDoneResult(result == DoneWith::Success); }; } }; class TASKING_EXPORT TaskTree final : public QObject { Q_OBJECT public: TaskTree(); TaskTree(const Group &recipe); ~TaskTree(); void setRecipe(const Group &recipe); void start(); void cancel(); bool isRunning() const; // Helper methods. They execute a local event loop with ExcludeUserInputEvents. // The passed future is used for listening to the cancel event. // Don't use it in main thread. To be used in non-main threads or in auto tests. DoneWith runBlocking(); DoneWith runBlocking(const QFuture &future); static DoneWith runBlocking(const Group &recipe, std::chrono::milliseconds timeout = std::chrono::milliseconds::max()); static DoneWith runBlocking(const Group &recipe, const QFuture &future, std::chrono::milliseconds timeout = std::chrono::milliseconds::max()); int asyncCount() const; int taskCount() const; int progressMaximum() const { return taskCount(); } int progressValue() const; // all finished / skipped / stopped tasks, groups itself excluded template void onStorageSetup(const Storage &storage, Handler &&handler) { static_assert(std::is_invocable_v, StorageStruct &>, "Storage setup handler needs to take (Storage &) as an argument. " "The passed handler doesn't fulfill this requirement."); setupStorageHandler(storage, wrapHandler(std::forward(handler)), {}); } template void onStorageDone(const Storage &storage, Handler &&handler) { static_assert(std::is_invocable_v, const StorageStruct &>, "Storage done handler needs to take (const Storage &) as an argument. " "The passed handler doesn't fulfill this requirement."); setupStorageHandler(storage, {}, wrapHandler(std::forward(handler))); } Q_SIGNALS: void started(); void done(DoneWith result); void asyncCountChanged(int count); void progressValueChanged(int value); // updated whenever task finished / skipped / stopped private: void setupStorageHandler(const StorageBase &storage, StorageBase::StorageHandler setupHandler, StorageBase::StorageHandler doneHandler); template StorageBase::StorageHandler wrapHandler(Handler &&handler) { return [handler](void *voidStruct) { auto *storageStruct = static_cast(voidStruct); std::invoke(handler, *storageStruct); }; } TaskTreePrivate *d; }; class TASKING_EXPORT TaskTreeTaskAdapter : public TaskAdapter { public: TaskTreeTaskAdapter(); private: void start() final; }; class TASKING_EXPORT TimeoutTaskAdapter : public TaskAdapter { public: TimeoutTaskAdapter(); ~TimeoutTaskAdapter(); private: void start() final; std::optional m_timerId; }; using TaskTreeTask = CustomTask; using TimeoutTask = CustomTask; } // namespace Tasking QT_END_NAMESPACE #endif // TASKING_TASKTREE_H