VLink  2.0.0
A high-performance communication middleware
graph_task.h
Go to the documentation of this file.
1 /*
2  * Copyright (C) 2026 by Thun Lu. All rights reserved.
3  * Author: Thun Lu <thun.lu@zohomail.cn>
4  * Repo: https://github.com/thun-res/vlink
5  * _ __ __ _ __
6  * | | / / / / (_) ____ / /__
7  * | | / / / / / / / __ \ / //_/
8  * | |/ / / /___ / / / / / / / ,<
9  * |___/ /_____/ /_/ /_/ /_/ /_/|_|
10  *
11  * Licensed under the Apache License, Version 2.0 (the "License");
12  * you may not use this file except in compliance with the License.
13  * You may obtain a copy of the License at
14  *
15  * http://www.apache.org/licenses/LICENSE-2.0
16  *
17  * Unless required by applicable law or agreed to in writing, software
18  * distributed under the License is distributed on an "AS IS" BASIS,
19  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20  * See the License for the specific language governing permissions and
21  * limitations under the License.
22  */
23 
24 /**
25  * @file graph_task.h
26  * @brief Directed acyclic task graph with condition branching, cycle guard and DOT export.
27  *
28  * @details
29  * @c GraphTask is VLink's data-flow primitive: every node carries a callback, a list of
30  * predecessors and a list of successors, and is submitted to any engine that exposes
31  * @c post_task / @c post_task_with_priority -- @c MessageLoop, @c MultiLoop or @c ThreadPool.
32  * Edges are declared with @c precede / @c succeed (Taskflow convention) and a process-wide
33  * topology mutex serialises all mutators across every graph instance.
34  *
35  * @par DAG diagram
36  *
37  * @verbatim
38  * +---+ precede +---+ precede +---+
39  * | A | ----------> | B | ----------> | C |
40  * +-+-+ +-+-+ +-+-+
41  * | condition | |
42  * v v v
43  * +-----+ +-----+ +-----+
44  * | D0 | | E | | F |
45  * +-----+ +-----+ +-----+
46  * ^
47  * | D0/D1/... condition task selects which successor branch fires
48  * +-----+
49  * | D1 |
50  * +-----+
51  * @endverbatim
52  *
53  * @par Task factories
54  *
55  * | Factory | Callback signature | Use case |
56  * | ----------------------------- | ------------------- | ------------------------------------- |
57  * | @c create(callback) | @c void() | Regular work node |
58  * | @c create_condition(callback) | @c int() | Branch selector (return value picks) |
59  *
60  * @par Execution policies
61  *
62  * | Policy | Behaviour |
63  * | ------------------ | ------------------------------------------------------------------ |
64  * | @c kPolicyOnce | Runs exactly once per @c execute pass (default) |
65  * | @c kPolicyMultiple | Runs multiple times within one @c execute pass |
66  * | @c kPolicyWaitAll | Waits for every predecessor before running |
67  *
68  * @par Example
69  * @code
70  * vlink::MultiLoop engine(4);
71  * engine.async_run();
72  *
73  * auto load = vlink::GraphTask::create("load", [] { load_data(); });
74  * auto proc = vlink::GraphTask::create("proc", [] { process(); });
75  * auto save = vlink::GraphTask::create("save", [] { save_data(); });
76  *
77  * // load -- > proc means load->precede(proc); produces execution order load, proc, save.
78  * load-- > proc-- > save;
79  *
80  * assert(!load->has_cycle());
81  * load->execute(&engine);
82  * @endcode
83  */
84 
85 #pragma once
86 
87 #include <memory>
88 #include <string>
89 #include <type_traits>
90 #include <unordered_set>
91 #include <utility>
92 #include <vector>
93 
94 #include "./functional.h"
95 #include "./macros.h"
96 #include "./traits.h"
97 
98 namespace vlink {
99 
100 /**
101  * @class GraphTask
102  * @brief DAG node carrying a callback, predecessor and successor links, and run-time state.
103  *
104  * @details
105  * Must be created through the @c create / @c create_condition factories so callbacks share
106  * @c std::enable_shared_from_this safely. @c execute traverses the reachable sub-graph and
107  * submits ready tasks to a user-supplied engine.
108  */
109 class VLINK_EXPORT GraphTask final : public std::enable_shared_from_this<GraphTask> {
110  public:
111  /**
112  * @brief Run-time execution state of a node within an @c execute pass.
113  */
114  enum Status : uint8_t {
115  kStatusInActive = 0, ///< Not yet submitted or cancelled.
116  kStatusPending = 1, ///< Waiting for predecessors to complete.
117  kStatusRunning = 2, ///< Currently executing.
118  kStatusDone = 3, ///< Execution finished.
119  };
120 
121  /**
122  * @brief Policy controlling how many times the task may run within a pass.
123  */
124  enum Policy : uint8_t {
125  kPolicyOnce = 0, ///< Run exactly once per @c execute pass (default).
126  kPolicyMultiple = 1, ///< Allow multiple invocations within the pass.
127  kPolicyWaitAll = 2, ///< Wait for every predecessor before running.
128  };
129 
130  /**
131  * @brief Callback type for void-returning work nodes.
132  */
133  using Callback = MoveFunction<void()>;
134 
135  /**
136  * @brief Callback type for condition nodes; the return value selects an outgoing branch.
137  */
139 
140  /**
141  * @brief Status-change notification callback.
142  *
143  * @details
144  * Invoked with the task name and the new status whenever the node transitions.
145  */
146  using StatusCallback = MoveFunction<void(const std::string&, Status)>;
147 
148  /**
149  * @brief Creates a regular work node.
150  *
151  * @param callback Work function.
152  * @param condition_number Number of outgoing branches (@c 0 disables branching).
153  * @return Shared pointer to the new task.
154  */
155  [[nodiscard]] static std::shared_ptr<GraphTask> create(Callback&& callback, int condition_number = 0);
156 
157  /**
158  * @brief Creates a named regular work node.
159  *
160  * @param name Node name used in DOT output and status callbacks.
161  * @param callback Work function.
162  * @param condition_number Number of outgoing branches.
163  * @return Shared pointer to the new task.
164  */
165  [[nodiscard]] static std::shared_ptr<GraphTask> create(const std::string& name, Callback&& callback,
166  int condition_number = 0);
167 
168  /**
169  * @brief Creates a condition node whose return value selects a successor branch.
170  *
171  * @param callback Predicate returning the branch index.
172  * @param condition_number Number of branches accepted; out-of-range returns skip all successors.
173  * @return Shared pointer to the new condition task.
174  */
175  [[nodiscard]] static std::shared_ptr<GraphTask> create_condition(ConditionCallback&& callback,
176  int condition_number = 0);
177 
178  /**
179  * @brief Creates a named condition node.
180  *
181  * @param name Node name.
182  * @param callback Predicate returning the branch index.
183  * @param condition_number Branch count.
184  * @return Shared pointer to the new condition task.
185  */
186  [[nodiscard]] static std::shared_ptr<GraphTask> create_condition(const std::string& name,
187  ConditionCallback&& callback,
188  int condition_number = 0);
189 
190  /**
191  * @brief Submits the reachable sub-graph to @p graph_engine.
192  *
193  * @details
194  * Traverses the sub-graph, identifies ready nodes and posts them via @c post_task or
195  * @c post_task_with_priority when available. Compatible with @c MessageLoop, @c MultiLoop and
196  * @c ThreadPool.
197  *
198  * @tparam GraphEngineT Engine type exposing @c post_task and optionally @c post_task_with_priority.
199  * @param graph_engine Target engine instance.
200  */
201  template <class GraphEngineT>
202  void execute(GraphEngineT* graph_engine);
203 
204  /**
205  * @brief Cancels this node and propagates the cancellation downstream.
206  *
207  * @details
208  * Sets this node's status to @c kStatusInActive and walks the successor list forward marking
209  * every reachable node inactive. Predecessors are unaffected. Inactive nodes are skipped
210  * by the engine and by downstream @c kPolicyWaitAll counters.
211  */
212  void cancel();
213 
214  /**
215  * @brief Declares that this node must complete before @p task starts.
216  *
217  * @details
218  * Runs a reachability pre-check on @p task 's downstream cone before mutating any list; the
219  * edge is rejected (with an error log) if it would form a cycle. On success @p task is
220  * appended to this node's successor list and this node is appended to @p task 's predecessor
221  * list. Topology mutation is serialised by a process-wide recursive mutex shared across
222  * every @c GraphTask instance, so concurrent writers are safe; read paths (@c execute,
223  * @c has_cycle, @c export_to_dot) read per-node snapshots without taking that mutex.
224  *
225  * @param task Successor node.
226  */
227  void precede(const std::shared_ptr<GraphTask>& task);
228 
229  /**
230  * @brief Declares that @p task must complete before this node starts.
231  *
232  * @details
233  * Mirror of @c precede; rejects edges that would form a cycle. Shares the same single-writer
234  * topology mutex as @c precede.
235  *
236  * @param task Predecessor node.
237  */
238  void succeed(const std::shared_ptr<GraphTask>& task);
239 
240  /**
241  * @brief Subscribes a callback to status transitions on this node.
242  *
243  * @details
244  * Each call appends a new subscriber. On every status change the node snapshots the current
245  * subscriber set under @c status_callbacks_mtx, releases the lock, then invokes the snapshot
246  * in unspecified order. Because callbacks fire without the mutex held they may freely
247  * register / unregister callbacks on the same node; such mutations apply on the next
248  * transition. Exceptions are caught and logged; remaining subscribers still fire.
249  *
250  * @param callback Status change callback.
251  * @return Subscription id (>0). Returns @c 0 when @p callback is empty.
252  */
254 
255  /**
256  * @brief Removes a previously registered status callback by id.
257  *
258  * @param id Subscription id returned by @c register_status_callback.
259  * @return @c true when the subscription was found and removed.
260  */
261  bool unregister_status_callback(uint32_t id);
262 
263  /**
264  * @brief Removes every status callback subscription on this node.
265  */
267 
268  /**
269  * @brief Sets the node name used in DOT output and status callbacks.
270  *
271  * @param name Node name.
272  */
273  void set_name(const std::string& name);
274 
275  /**
276  * @brief Sets a group name used to visually cluster nodes in DOT output.
277  *
278  * @param name Group name.
279  */
280  void set_group_name(const std::string& name);
281 
282  /**
283  * @brief Sets the number of outgoing condition branches.
284  *
285  * @param condition_number Branch count.
286  */
287  void set_condition_number(int condition_number);
288 
289  /**
290  * @brief Sets the dispatch priority used by priority-aware engines.
291  *
292  * @param priority Priority value.
293  */
294  void set_priority(uint16_t priority);
295 
296  /**
297  * @brief Sets the maximum recursion depth that bounds DFS traversals.
298  *
299  * @details
300  * Traversals exceeding this depth treat the graph as conservatively cyclic. Default: @c 10000.
301  *
302  * @param depth Maximum recursion depth.
303  */
304  void set_max_recursion_depth(uint32_t depth);
305 
306  /**
307  * @brief Sets the execution policy for this node.
308  *
309  * @param policy Policy enumerator.
310  */
311  void set_policy(Policy policy);
312 
313  /**
314  * @brief Returns the node name.
315  *
316  * @return Node name.
317  */
318  [[nodiscard]] std::string get_name() const;
319 
320  /**
321  * @brief Returns the group name.
322  *
323  * @return Group name.
324  */
325  [[nodiscard]] std::string get_group_name() const;
326 
327  /**
328  * @brief Returns the configured branch count.
329  *
330  * @return Branch count.
331  */
332  [[nodiscard]] int get_condition_number() const;
333 
334  /**
335  * @brief Returns the configured dispatch priority.
336  *
337  * @return Priority value.
338  */
339  [[nodiscard]] uint16_t get_priority() const;
340 
341  /**
342  * @brief Returns the maximum recursion depth used for cycle detection.
343  *
344  * @return Recursion depth bound.
345  */
346  [[nodiscard]] uint32_t get_max_recursion_depth() const;
347 
348  /**
349  * @brief Returns the configured execution policy.
350  *
351  * @return Policy enumerator.
352  */
353  [[nodiscard]] Policy get_policy() const;
354 
355  /**
356  * @brief Returns the current execution status of this node.
357  *
358  * @return Status enumerator.
359  */
360  [[nodiscard]] Status get_status() const;
361 
362  /**
363  * @brief Removes an outgoing edge created by @c precede.
364  *
365  * @details
366  * Removes @p task from the successor list and removes this node from @p task 's predecessor
367  * list. Logs an error when the edge does not exist.
368  *
369  * @param task Previously attached successor.
370  */
371  void remove_precede_task(const std::shared_ptr<GraphTask>& task);
372 
373  /**
374  * @brief Removes an incoming edge created by @c succeed.
375  *
376  * @param task Previously attached predecessor.
377  */
378  void remove_succeed_task(const std::shared_ptr<GraphTask>& task);
379 
380  /**
381  * @brief Returns the current predecessor list as weak pointers.
382  *
383  * @return Vector of weak predecessor pointers.
384  */
385  [[nodiscard]] std::vector<std::weak_ptr<GraphTask>> get_precede_task_list() const;
386 
387  /**
388  * @brief Returns the current successor list as weak pointers.
389  *
390  * @return Vector of weak successor pointers.
391  */
392  [[nodiscard]] std::vector<std::weak_ptr<GraphTask>> get_succeed_task_list() const;
393 
394  /**
395  * @brief Reports whether this node was created via @c create_condition.
396  *
397  * @return @c true for condition nodes.
398  */
399  [[nodiscard]] bool is_condition_task() const;
400 
401  /**
402  * @brief Detects whether the reachable sub-graph contains a cycle.
403  *
404  * @details
405  * Iterative DFS with a recursion stack. Acquires per-node mutexes one at a time and does
406  * not take the global topology mutex; safe to call from status callbacks fired by @c invoke
407  * or @c cancel.
408  *
409  * @return @c true when a cycle is found or the recursion bound is exceeded.
410  */
411  [[nodiscard]] bool has_cycle() const;
412 
413  /**
414  * @brief Exports the reachable sub-graph as a Graphviz DOT document.
415  *
416  * @return DOT source string.
417  */
418  [[nodiscard]] std::string export_to_dot() const;
419 
420  protected:
421  using FindTaskCallback = MoveFunction<void(const std::shared_ptr<GraphTask>&)>;
422 
423  explicit GraphTask(Callback&& callback, int condition_number);
424 
425  explicit GraphTask(const std::string& name, Callback&& callback, int condition_number);
426 
427  explicit GraphTask(ConditionCallback&& callback, int condition_number);
428 
429  explicit GraphTask(const std::string& name, ConditionCallback&& callback, int condition_number);
430 
432 
434 
435  private:
436  int invoke(bool once);
437 
438  void wait();
439 
440  void notify(int condition_number);
441 
442  void notify_skip();
443 
444  bool mark_predecessor_satisfied(bool active, bool* has_active);
445 
446  void update_status(Status status);
447 
448  bool detect_cycle(const GraphTask* task, std::unordered_set<const GraphTask*>& visited,
449  std::unordered_set<const GraphTask*>& recursion_stack, uint32_t& depth, uint32_t max_depth) const;
450 
451  bool reaches_via_successors(const GraphTask* start_node,
452  const std::vector<std::weak_ptr<GraphTask>>& start_successors,
453  const GraphTask* target) const;
454 
455  static void clear_invalid_task(const std::shared_ptr<GraphTask>& task);
456 
457  struct Impl;
458  std::unique_ptr<Impl> impl_;
459 
461 };
462 
463 /**
464  * @typedef GraphTaskPtr
465  * @brief Convenience alias for the canonical @c shared_ptr<GraphTask> handle.
466  */
467 using GraphTaskPtr = std::shared_ptr<GraphTask>;
468 
469 ////////////////////////////////////////////////////////////////
470 /// Details
471 ////////////////////////////////////////////////////////////////
472 
473 template <class GraphEngineT>
474 inline void GraphTask::execute(GraphEngineT* graph_engine) {
475  auto self = shared_from_this();
476 
477  process_and_traverse([self, graph_engine](const std::shared_ptr<GraphTask>& task) {
478  constexpr bool kHaspriority = VLINK_HAS_MEMBER(GraphEngineT, post_task_with_priority);
479  [[maybe_unused]] constexpr uint8_t kPriorityType = 2;
480 
481  if VUNLIKELY (task->get_status() == kStatusInActive) {
482  return;
483  }
484 
485  auto task_func = [self, task]() {
486  if VLIKELY (task.get() != self.get()) {
487  task->wait();
488  }
489 
490  int ret = task->invoke(true);
491 
492  if VLIKELY (ret >= 0) {
493  task->notify(ret);
494  }
495  };
496 
497  auto post_task = [graph_engine](auto&& func) -> bool {
498  using Ret = decltype(graph_engine->post_task(std::forward<decltype(func)>(func)));
499 
500  if constexpr (std::is_same_v<Ret, bool>) {
501  return graph_engine->post_task(std::forward<decltype(func)>(func));
502  } else {
503  graph_engine->post_task(std::forward<decltype(func)>(func));
504  return true;
505  }
506  };
507 
508  bool posted = false;
509 
510  if constexpr (kHaspriority) {
511  auto post_task_with_priority = [graph_engine, task](auto&& func) -> bool {
512  using Ret =
513  decltype(graph_engine->post_task_with_priority(std::forward<decltype(func)>(func), task->get_priority()));
514 
515  if constexpr (std::is_same_v<Ret, bool>) {
516  return graph_engine->post_task_with_priority(std::forward<decltype(func)>(func), task->get_priority());
517  } else {
518  graph_engine->post_task_with_priority(std::forward<decltype(func)>(func), task->get_priority());
519  return true;
520  }
521  };
522 
523  if constexpr (VLINK_HAS_MEMBER(GraphEngineT, get_type)) {
524  if (graph_engine->get_type() == kPriorityType) {
525  posted = post_task_with_priority(std::move(task_func));
526  } else {
527  posted = post_task(std::move(task_func));
528  }
529  } else {
530  posted = post_task(std::move(task_func));
531  }
532  } else {
533  posted = post_task(std::move(task_func));
534  }
535 
536  if VUNLIKELY (!posted) {
537  task->cancel();
538  }
539  });
540 }
541 
542 [[maybe_unused]] static inline GraphTaskPtr& operator--(GraphTaskPtr& task, int) { return task; }
543 
544 [[maybe_unused]] static inline GraphTaskPtr& operator>(GraphTaskPtr& task, GraphTaskPtr& target_task) {
545  task->precede(target_task);
546  return target_task;
547 }
548 
549 [[maybe_unused]] static inline GraphTaskPtr& operator<(GraphTaskPtr& task, GraphTaskPtr& target_task) {
550  task->succeed(target_task);
551  return target_task;
552 }
553 
554 [[maybe_unused]] static inline GraphTaskPtr& operator--(GraphTaskPtr& task) { return task; }
555 
556 } // namespace vlink
Pool-backed type-erased callables: copyable vlink::Function and move-only vlink::MoveFunction.
Cross-platform macros for visibility, branch hints, copy prevention, singletons and string helpers.
#define VUNLIKELY(...)
Short alias for VLINK_UNLIKELY.
Definition: macros.h:289
#define VLINK_EXPORT
Definition: macros.h:81
#define VLIKELY(...)
Short alias for VLINK_LIKELY.
Definition: macros.h:284
#define VLINK_DISALLOW_COPY_AND_ASSIGN(classname)
Deletes the copy constructor and copy-assignment operator of classname.
Definition: macros.h:174
Compile-time type-trait helpers used across the VLink codebase.
#define VLINK_HAS_MEMBER(T, member)
Macro Definitions.
Definition: traits.h:316