Line data Source code
1 : // 2 : // Copyright 2024 OpenModelViewer Authors 3 : // 4 : // Licensed under the Apache License, Version 2.0 (the "License"); 5 : // you may not use this file except in compliance with the License. 6 : // You may obtain a copy of the License at 7 : // 8 : // http://www.apache.org/licenses/LICENSE-2.0 9 : // 10 : // Unless required by applicable law or agreed to in writing, software 11 : // distributed under the License is distributed on an "AS IS" BASIS, 12 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 : // See the License for the specific language governing permissions and 14 : // limitations under the License. 15 : // 16 : 17 : #pragma once 18 : 19 : #include "openmodelviewer/core/async/itask_data.hpp" 20 : #include "openmodelviewer/core/async/task_result.hpp" 21 : 22 : #include <atomic> 23 : #include <optional> 24 : #include <exception> 25 : #include <stdexcept> 26 : #include <mutex> 27 : #include <functional> 28 : #include <utility> 29 : 30 : 31 : namespace openmodelviewer::core::async 32 : { 33 : /** 34 : * @brief Stores the result, completion, and error state of an asynchronous task. 35 : * 36 : * This class is used internally by the ThreadPool to track the execution state 37 : * and return data of a task. It supports both successful resolution and 38 : * exception reporting. The result is stored in an optional container, 39 : * while the error is stored using std::exception_ptr. 40 : * 41 : * @tparam RType The result type produced by the task. 42 : */ 43 : template <typename RType> 44 : class TaskData : public ITaskData 45 : { 46 : public: 47 : /** 48 : * @brief Returns whether the task has completed, either successfully or with an error. 49 : * 50 : * A task is considered completed if it has either returned a data 51 : * or thrown an exception during execution. 52 : */ 53 236 : inline bool completed() const noexcept override 54 : { 55 236 : return m_completed.load(std::memory_order_acquire); 56 : } 57 : 58 : /** 59 : * @brief Returns whether the task ended with an error. 60 : * 61 : * @return true if the task failed with an exception. 62 : */ 63 22 : inline bool errored() const noexcept override 64 : { 65 22 : return m_errored.load(std::memory_order_acquire); 66 : } 67 : 68 : /** 69 : * @brief Returns the stored exception, if any. 70 : * 71 : * This can be used to rethrow the exception from another thread context. 72 : * 73 : * @return The exception_ptr representing the captured error. 74 : */ 75 3 : inline std::exception_ptr exception() const noexcept override 76 : { 77 3 : return m_exception; 78 : } 79 : 80 : /** 81 : * @brief Checks if a callback has been registered for this task. 82 : * 83 : * This method returns true if a callback function is set and ready to be invoked 84 : * once the task completes. A registered callback will be automatically called 85 : * with a TaskResult<RType> when the task finishes (successfully or with an error). 86 : * 87 : * @return true if a callback is registered; false otherwise. 88 : */ 89 94 : inline bool hasCallback() const noexcept override 90 : { 91 94 : return m_callback != nullptr; 92 : } 93 : 94 : /** 95 : * @brief Invokes the registered callback with the task result. 96 : * 97 : * This method constructs a TaskResult<RType> using the task�s current state, 98 : * then calls the registered callback function if the task has completed and 99 : * a callback is present. After execution, the callback is cleared. 100 : * 101 : * This should be called exactly once after the task has completed. 102 : * If no callback is registered, nothing happens. 103 : */ 104 97 : inline void invokeCallback() override 105 : { 106 97 : if (this->completed() && this->hasCallback()) 107 : { 108 107 : TaskResult<RType> tres = 109 : { 110 28 : .data = this->m_return, 111 79 : .error = this->m_exception 112 : }; 113 74 : m_callback(tres); 114 76 : m_callback = nullptr; 115 74 : } 116 90 : } 117 : 118 : /** 119 : * @brief Stores the result and marks the task as successfully completed. 120 : * 121 : * This method should be called exactly once, by the executing thread. 122 : * 123 : * @param data The result produced by the task. 124 : */ 125 87 : inline void resolve(RType data) 126 : { 127 87 : if (m_completed.load()) 128 : { 129 1 : throw std::runtime_error("Trying to resolve the same TaskData multiple times."); 130 : } 131 : 132 90 : m_return = std::move(data); 133 89 : m_completed.store(true, std::memory_order_release); 134 87 : } 135 : 136 : /** 137 : * @brief Reports an exception and marks the task as completed with an error. 138 : * 139 : * This method should be called if the task throws during execution. 140 : * 141 : * @param exception The exception_ptr representing the thrown error. 142 : */ 143 9 : inline void report(std::exception_ptr exception) 144 : { 145 9 : m_completed.store(true, std::memory_order_release); 146 9 : m_errored.store(true, std::memory_order_release); 147 9 : m_exception = std::move(exception); 148 9 : } 149 : 150 : /** 151 : * @brief Returns a const reference to the result data. 152 : * 153 : * The optional may be empty if the task is not completed, 154 : * or if the task failed with an exception. 155 : * 156 : * @return A const reference to the result container. 157 : */ 158 11 : inline const std::optional<RType>& retrieve() const noexcept 159 : { 160 11 : return m_return; 161 : } 162 : 163 : /** 164 : * @brief Rethrows the exception if the task ended with an error. 165 : * 166 : * Use this to propagate exceptions in user code after checking completion. 167 : * @throws The exception originally thrown by the task. 168 : */ 169 3 : inline void rethrow() const 170 : { 171 3 : if (m_errored.load()) 172 : { 173 3 : std::rethrow_exception(m_exception); 174 : } 175 0 : } 176 : 177 : /** 178 : * @brief Stores a callback to be invoked when the task completes successfully or with an error. 179 : * 180 : * If the task has already completed at the time of registration, the callback is invoked 181 : * immediately on the calling thread with the current TaskResult. 182 : * 183 : * After the callback is executed, it is cleared from memory to avoid redundant invocations. 184 : * 185 : * Thread-safe: protects internal state with a mutex to allow safe concurrent usage. 186 : * 187 : * @param callback A callable that takes a const reference to TaskResult<RType>. 188 : */ 189 86 : inline void setCallback(std::function<void(const TaskResult<RType>&)> callback) 190 : { 191 86 : std::lock_guard lock(m_callbackMutex); 192 86 : m_callback = std::move(callback); 193 : 194 86 : if (m_completed) 195 : { 196 5 : TaskResult<RType> result 197 : { 198 1 : .data = m_return, 199 4 : .error = m_exception 200 : }; 201 : 202 4 : m_callback(result); 203 4 : m_callback = nullptr; 204 4 : } 205 86 : } 206 : 207 : private: 208 : std::atomic<bool> m_errored{ false }; 209 : std::exception_ptr m_exception; 210 : std::atomic<bool> m_completed{ false }; 211 : std::optional<RType> m_return; 212 : 213 : std::mutex m_callbackMutex; 214 : std::function<void(const TaskResult<RType>&)> m_callback; 215 : }; 216 : } // namespace openmodelviewer::core::async