├── .github
└── workflows
│ ├── release.yml
│ └── test.yml
├── .gitignore
├── .php_cs
├── README.md
├── composer.json
├── example
└── example.php
├── phpunit.xml
├── src
├── Dag.php
├── Exception
│ └── InvalidArgumentException.php
├── Runner.php
└── Vertex.php
└── tests
├── Cases
└── DagTest.php
└── bootstrap.php
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | # Sequence of patterns matched against refs/tags
4 | tags:
5 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
6 |
7 | name: Release
8 |
9 | jobs:
10 | release:
11 | name: Release
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: Checkout code
15 | uses: actions/checkout@v2
16 | - name: Create Release
17 | id: create_release
18 | uses: actions/create-release@v1
19 | env:
20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
21 | with:
22 | tag_name: ${{ github.ref }}
23 | release_name: Release ${{ github.ref }}
24 | draft: false
25 | prerelease: false
26 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: PHPUnit
2 |
3 | on: [push, pull_request]
4 |
5 | env:
6 | SWOOLE_VERSION: '4.6.1'
7 | SWOW_VERSION: 'develop'
8 |
9 | jobs:
10 | ci:
11 | name: Test PHP ${{ matrix.php-version }} on ${{ matrix.engine }}
12 | runs-on: "${{ matrix.os }}"
13 | strategy:
14 | matrix:
15 | os: [ubuntu-latest]
16 | php-version: ['7.3', '7.4']
17 | engine: ['swoole']
18 | max-parallel: 5
19 | steps:
20 | - name: Checkout
21 | uses: actions/checkout@v2
22 | - name: Setup PHP
23 | uses: shivammathur/setup-php@v2
24 | with:
25 | php-version: ${{ matrix.php-version }}
26 | tools: phpize
27 | ini-values: opcache.enable_cli=1
28 | coverage: none
29 | - name: Setup Swoole
30 | if: ${{ matrix.engine == 'swoole' }}
31 | run: |
32 | wget https://github.com/swoole/swoole-src/archive/v${SWOOLE_VERSION}.tar.gz -O swoole.tar.gz
33 | mkdir -p swoole
34 | tar -xf swoole.tar.gz -C swoole --strip-components=1
35 | rm swoole.tar.gz
36 | cd swoole
37 | phpize
38 | ./configure --enable-openssl --enable-mysqlnd --enable-http2
39 | make -j$(nproc)
40 | sudo make install
41 | sudo sh -c "echo extension=swoole > /etc/php/${{ matrix.php-version }}/cli/conf.d/swoole.ini"
42 | php --ri swoole
43 | - name: Setup Swow
44 | if: ${{ matrix.engine == 'swow' }}
45 | run: |
46 | wget https://github.com/swow/swow/archive/"${SWOW_VERSION}".tar.gz -O swow.tar.gz
47 | mkdir -p swow
48 | tar -xf swow.tar.gz -C swow --strip-components=1
49 | rm swow.tar.gz
50 | cd swow/ext || exit
51 |
52 | phpize
53 | ./configure --enable-debug
54 | make -j "$(nproc)"
55 | sudo make install
56 | sudo sh -c "echo extension=swow > /etc/php/${{ matrix.php-version }}/cli/conf.d/swow.ini"
57 | php --ri swow
58 | - name: Setup Packages
59 | run: composer update -o --no-scripts
60 | - name: Run Test Cases
61 | run: |
62 | composer analyse
63 | composer test
64 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor/
2 | composer.lock
3 | *.cache
4 | *.log
--------------------------------------------------------------------------------
/.php_cs:
--------------------------------------------------------------------------------
1 | setRiskyAllowed(true)
14 | ->setRules([
15 | '@PSR2' => true,
16 | '@Symfony' => true,
17 | '@DoctrineAnnotation' => true,
18 | '@PhpCsFixer' => true,
19 | 'header_comment' => [
20 | 'commentType' => 'PHPDoc',
21 | 'header' => $header,
22 | 'separate' => 'none',
23 | 'location' => 'after_declare_strict',
24 | ],
25 | 'array_syntax' => [
26 | 'syntax' => 'short'
27 | ],
28 | 'list_syntax' => [
29 | 'syntax' => 'short'
30 | ],
31 | 'concat_space' => [
32 | 'spacing' => 'one'
33 | ],
34 | 'blank_line_before_statement' => [
35 | 'statements' => [
36 | 'declare',
37 | ],
38 | ],
39 | 'general_phpdoc_annotation_remove' => [
40 | 'annotations' => [
41 | 'author'
42 | ],
43 | ],
44 | 'ordered_imports' => [
45 | 'imports_order' => [
46 | 'class', 'function', 'const',
47 | ],
48 | 'sort_algorithm' => 'alpha',
49 | ],
50 | 'single_line_comment_style' => [
51 | 'comment_types' => [
52 | ],
53 | ],
54 | 'yoda_style' => [
55 | 'always_move_variable' => false,
56 | 'equal' => false,
57 | 'identical' => false,
58 | ],
59 | 'phpdoc_align' => [
60 | 'align' => 'left',
61 | ],
62 | 'multiline_whitespace_before_semicolons' => [
63 | 'strategy' => 'no_multi_line',
64 | ],
65 | 'constant_case' => [
66 | 'case' => 'lower',
67 | ],
68 | 'class_attributes_separation' => true,
69 | 'combine_consecutive_unsets' => true,
70 | 'declare_strict_types' => true,
71 | 'linebreak_after_opening_tag' => true,
72 | 'lowercase_static_reference' => true,
73 | 'no_useless_else' => true,
74 | 'no_unused_imports' => true,
75 | 'not_operator_with_successor_space' => true,
76 | 'not_operator_with_space' => false,
77 | 'ordered_class_elements' => true,
78 | 'php_unit_strict' => false,
79 | 'phpdoc_separation' => false,
80 | 'single_quote' => true,
81 | 'standardize_not_equals' => true,
82 | 'multiline_comment_opening_closing' => true,
83 | ])
84 | ->setFinder(
85 | PhpCsFixer\Finder::create()
86 | ->exclude('vendor')
87 | ->in(__DIR__)
88 | )
89 | ->setUsingCache(false);
90 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | `hyperf/dag-incubator` 是一个轻量级有向无环图 (**D**irected **A**cyclic **G**raph) 任务编排库。
2 |
3 | ## 场景
4 |
5 | 假设我们有一系列任务需要执行。
6 |
7 | - 如果他们之间存在依赖关系,则可以将他们顺序执行。
8 | - 如果他们并不相互依赖,那么我们可以选择并发执行,以加快执行速度。
9 | - 两者间还存在中间状态:一部分任务存在依赖关系,而另一些任务又可以并发执行。
10 |
11 | 我们可以将第三种复杂的场景抽象成 `DAG` 来解决。
12 |
13 | ## 示例
14 |
15 | [](https://mermaid-js.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoic3RhdGVEaWFncmFtLXYyXG4gICAgWypdIC0tPiBBXG4gICAgQSAtLT4gQlxuICAgIEEgLS0-IENcbiAgICBBIC0tPiBEXG4gICAgRCAtLT4gR1xuICAgIEMgLS0-IEdcbiAgICBDIC0tPiBGXG4gICAgQiAtLT4gRlxuICAgIEIgLS0-IEVcbiAgICBCIC0tPiBIXG4gICAgSCAtLT4gSVxuICAgIEUgLS0-IElcbiAgICBGIC0tPiBJXG4gICAgRyAtLT4gSVxuICAgIEkgLS0-IFsqXVxuICAgICAgICAgICAgIiwibWVybWFpZCI6eyJ0aGVtZSI6ImRlZmF1bHQiLCJ0aGVtZVZhcmlhYmxlcyI6eyJiYWNrZ3JvdW5kIjoid2hpdGUiLCJwcmltYXJ5Q29sb3IiOiIjRUNFQ0ZGIiwic2Vjb25kYXJ5Q29sb3IiOiIjZmZmZmRlIiwidGVydGlhcnlDb2xvciI6ImhzbCg4MCwgMTAwJSwgOTYuMjc0NTA5ODAzOSUpIiwicHJpbWFyeUJvcmRlckNvbG9yIjoiaHNsKDI0MCwgNjAlLCA4Ni4yNzQ1MDk4MDM5JSkiLCJzZWNvbmRhcnlCb3JkZXJDb2xvciI6ImhzbCg2MCwgNjAlLCA4My41Mjk0MTE3NjQ3JSkiLCJ0ZXJ0aWFyeUJvcmRlckNvbG9yIjoiaHNsKDgwLCA2MCUsIDg2LjI3NDUwOTgwMzklKSIsInByaW1hcnlUZXh0Q29sb3IiOiIjMTMxMzAwIiwic2Vjb25kYXJ5VGV4dENvbG9yIjoiIzAwMDAyMSIsInRlcnRpYXJ5VGV4dENvbG9yIjoicmdiKDkuNTAwMDAwMDAwMSwgOS41MDAwMDAwMDAxLCA5LjUwMDAwMDAwMDEpIiwibGluZUNvbG9yIjoiIzMzMzMzMyIsInRleHRDb2xvciI6IiMzMzMiLCJtYWluQmtnIjoiI0VDRUNGRiIsInNlY29uZEJrZyI6IiNmZmZmZGUiLCJib3JkZXIxIjoiIzkzNzBEQiIsImJvcmRlcjIiOiIjYWFhYTMzIiwiYXJyb3doZWFkQ29sb3IiOiIjMzMzMzMzIiwiZm9udEZhbWlseSI6IlwidHJlYnVjaGV0IG1zXCIsIHZlcmRhbmEsIGFyaWFsIiwiZm9udFNpemUiOiIxNnB4IiwibGFiZWxCYWNrZ3JvdW5kIjoiI2U4ZThlOCIsIm5vZGVCa2ciOiIjRUNFQ0ZGIiwibm9kZUJvcmRlciI6IiM5MzcwREIiLCJjbHVzdGVyQmtnIjoiI2ZmZmZkZSIsImNsdXN0ZXJCb3JkZXIiOiIjYWFhYTMzIiwiZGVmYXVsdExpbmtDb2xvciI6IiMzMzMzMzMiLCJ0aXRsZUNvbG9yIjoiIzMzMyIsImVkZ2VMYWJlbEJhY2tncm91bmQiOiIjZThlOGU4IiwiYWN0b3JCb3JkZXIiOiJoc2woMjU5LjYyNjE2ODIyNDMsIDU5Ljc3NjUzNjMxMjglLCA4Ny45MDE5NjA3ODQzJSkiLCJhY3RvckJrZyI6IiNFQ0VDRkYiLCJhY3RvclRleHRDb2xvciI6ImJsYWNrIiwiYWN0b3JMaW5lQ29sb3IiOiJncmV5Iiwic2lnbmFsQ29sb3IiOiIjMzMzIiwic2lnbmFsVGV4dENvbG9yIjoiIzMzMyIsImxhYmVsQm94QmtnQ29sb3IiOiIjRUNFQ0ZGIiwibGFiZWxCb3hCb3JkZXJDb2xvciI6ImhzbCgyNTkuNjI2MTY4MjI0MywgNTkuNzc2NTM2MzEyOCUsIDg3LjkwMTk2MDc4NDMlKSIsImxhYmVsVGV4dENvbG9yIjoiYmxhY2siLCJsb29wVGV4dENvbG9yIjoiYmxhY2siLCJub3RlQm9yZGVyQ29sb3IiOiIjYWFhYTMzIiwibm90ZUJrZ0NvbG9yIjoiI2ZmZjVhZCIsIm5vdGVUZXh0Q29sb3IiOiJibGFjayIsImFjdGl2YXRpb25Cb3JkZXJDb2xvciI6IiM2NjYiLCJhY3RpdmF0aW9uQmtnQ29sb3IiOiIjZjRmNGY0Iiwic2VxdWVuY2VOdW1iZXJDb2xvciI6IndoaXRlIiwic2VjdGlvbkJrZ0NvbG9yIjoicmdiYSgxMDIsIDEwMiwgMjU1LCAwLjQ5KSIsImFsdFNlY3Rpb25Ca2dDb2xvciI6IndoaXRlIiwic2VjdGlvbkJrZ0NvbG9yMiI6IiNmZmY0MDAiLCJ0YXNrQm9yZGVyQ29sb3IiOiIjNTM0ZmJjIiwidGFza0JrZ0NvbG9yIjoiIzhhOTBkZCIsInRhc2tUZXh0TGlnaHRDb2xvciI6IndoaXRlIiwidGFza1RleHRDb2xvciI6IndoaXRlIiwidGFza1RleHREYXJrQ29sb3IiOiJibGFjayIsInRhc2tUZXh0T3V0c2lkZUNvbG9yIjoiYmxhY2siLCJ0YXNrVGV4dENsaWNrYWJsZUNvbG9yIjoiIzAwMzE2MyIsImFjdGl2ZVRhc2tCb3JkZXJDb2xvciI6IiM1MzRmYmMiLCJhY3RpdmVUYXNrQmtnQ29sb3IiOiIjYmZjN2ZmIiwiZ3JpZENvbG9yIjoibGlnaHRncmV5IiwiZG9uZVRhc2tCa2dDb2xvciI6ImxpZ2h0Z3JleSIsImRvbmVUYXNrQm9yZGVyQ29sb3IiOiJncmV5IiwiY3JpdEJvcmRlckNvbG9yIjoiI2ZmODg4OCIsImNyaXRCa2dDb2xvciI6InJlZCIsInRvZGF5TGluZUNvbG9yIjoicmVkIiwibGFiZWxDb2xvciI6ImJsYWNrIiwiZXJyb3JCa2dDb2xvciI6IiM1NTIyMjIiLCJlcnJvclRleHRDb2xvciI6IiM1NTIyMjIiLCJjbGFzc1RleHQiOiIjMTMxMzAwIiwiZmlsbFR5cGUwIjoiI0VDRUNGRiIsImZpbGxUeXBlMSI6IiNmZmZmZGUiLCJmaWxsVHlwZTIiOiJoc2woMzA0LCAxMDAlLCA5Ni4yNzQ1MDk4MDM5JSkiLCJmaWxsVHlwZTMiOiJoc2woMTI0LCAxMDAlLCA5My41Mjk0MTE3NjQ3JSkiLCJmaWxsVHlwZTQiOiJoc2woMTc2LCAxMDAlLCA5Ni4yNzQ1MDk4MDM5JSkiLCJmaWxsVHlwZTUiOiJoc2woLTQsIDEwMCUsIDkzLjUyOTQxMTc2NDclKSIsImZpbGxUeXBlNiI6ImhzbCg4LCAxMDAlLCA5Ni4yNzQ1MDk4MDM5JSkiLCJmaWxsVHlwZTciOiJoc2woMTg4LCAxMDAlLCA5My41Mjk0MTE3NjQ3JSkifX0sInVwZGF0ZUVkaXRvciI6ZmFsc2V9)
16 |
17 | 假设我们有一系列任务,拓扑结构如上图所示,顶点代表任务,边缘代表依赖关系。(A完成后才能完成B、C、D,B完成后才能完成H、E、F...)
18 |
19 | 通过 `hyperf/dag-incubator` 可以使用如下方式构建 `DAG` 并执行。
20 |
21 | ```php
22 | addVertex($a)
34 | ->addVertex($b)
35 | ->addVertex($c)
36 | ->addVertex($d)
37 | ->addVertex($e)
38 | ->addVertex($f)
39 | ->addVertex($g)
40 | ->addVertex($h)
41 | ->addVertex($i)
42 | ->addEdge($a, $b)
43 | ->addEdge($a, $c)
44 | ->addEdge($a, $d)
45 | ->addEdge($b, $h)
46 | ->addEdge($b, $e)
47 | ->addEdge($b, $f)
48 | ->addEdge($c, $f)
49 | ->addEdge($c, $g)
50 | ->addEdge($d, $g)
51 | ->addEdge($h, $i)
52 | ->addEdge($e, $i)
53 | ->addEdge($f, $i)
54 | ->addEdge($g, $i);
55 |
56 | // 需要在协程环境下执行
57 | $dag->run();
58 |
59 | ```
60 |
61 | 输出:
62 |
63 | ```php
64 | // 1s 后
65 | A
66 | // 2s 后
67 | D
68 | C
69 | B
70 | // 3s 后
71 | G
72 | F
73 | E
74 | H
75 | // 4s 后
76 | I
77 | ```
78 |
79 | > DAG 会按照尽可能早的原则调度任务。尝试将 B 点的耗时调整为 2 秒,会发现 B 和 G 一起完成。
80 |
81 | ## 访问前步结果
82 |
83 | 每一个任务可以接收一个数组参数,数组中包含所有前置依赖的结果。`DAG` 执行完毕后,也会返回一个同样结构的数组,包含每一步的执行结果。
84 |
85 | ```php
86 | key] + 1;
91 | });
92 | $results = $dag->addVertex($a)->addVertex($b)->addEdge($a, $b)->run();
93 | assert($results[$a->key] === 1);
94 | assert($results[$b->key] === 2);
95 | ```
96 |
97 | ## 定义一个任务
98 |
99 | 在上述文档中,我们使用了闭包来定义一个任务。格式如下。
100 |
101 | ```php
102 | // Vertex::make 的第二个参数为可选参数,作为 vertex 的 key,也就是结果数组的键值。
103 | \Hyperf\Dag\Vertex::make(function() { return 'hello'; }, "greeting");
104 | ```
105 |
106 | 除了使用闭包函数定义任务外,还可以使用实现了 `\Hyperf\Dag\Runner` 接口的类来定义,并通过 `Vertex::of` 将其转化为一个顶点。
107 |
108 | ```php
109 | class MyJob implements \Hyperf\Dag\Runner {
110 | public function run($results = []) {
111 | return 'hello';
112 | }
113 | }
114 |
115 | \Hyperf\Dag\Vertex::of(new MyJob(), "greeting");
116 | ```
117 |
118 | `\Hyperf\Dag\Dag` 本身也实现了 `\Hyperf\Dag\Runner` 接口,所以可以嵌套使用。
119 |
120 | ```php
121 | addVertex($a)->addVertex($b)->addEdge($a, $b);
129 | $d = Vertex::of($nestedDag);
130 |
131 | $superDag = new Dag();
132 | $superDag->addVertex($c)->addVertex($d)->addEdge($c, $d);
133 | $superDag->run();
134 | ```
135 |
136 | ## 控制并发数
137 | `\Hyperf\Dag\Dag` 类提供了 `setConcurrency(int n)` 方法控制最大并发数。默认为10。
138 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hyperf/dag-incubator",
3 | "type": "library",
4 | "license": "MIT",
5 | "keywords": [
6 | "php",
7 | "hyperf"
8 | ],
9 | "description": "Dag runner for hyperf",
10 | "autoload": {
11 | "psr-4": {
12 | "Hyperf\\Dag\\": "src/"
13 | }
14 | },
15 | "autoload-dev": {
16 | "psr-4": {
17 | "HyperfTest\\": "tests"
18 | }
19 | },
20 | "require": {
21 | "php": ">=7.3",
22 | "hyperf/utils": "~2.1.0"
23 | },
24 | "require-dev": {
25 | "friendsofphp/php-cs-fixer": "^2.14",
26 | "hyperf/testing": "^2.1",
27 | "mockery/mockery": "^1.0",
28 | "phpstan/phpstan": "^0.12",
29 | "swoole/ide-helper": "dev-master",
30 | "swow/swow": "dev-develop",
31 | "symfony/var-dumper": "^5.1"
32 | },
33 | "config": {
34 | "sort-packages": true
35 | },
36 | "scripts": {
37 | "test": "co-phpunit -c phpunit.xml --colors=always",
38 | "analyse": "phpstan analyse --memory-limit 1024M -l 0 ./src",
39 | "cs-fix": "php-cs-fixer fix $1"
40 | },
41 | "extra": {
42 | "hyperf": {
43 | "config": "Hyperf\\Dag\\ConfigProvider"
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/example/example.php:
--------------------------------------------------------------------------------
1 | addVertex($a)
52 | ->addVertex($b)
53 | ->addVertex($c)
54 | ->addVertex($d)
55 | ->addVertex($e)
56 | ->addVertex($f)
57 | ->addVertex($g)
58 | ->addVertex($h)
59 | ->addVertex($i)
60 | ->addEdge($a, $i)
61 | ->addEdge($a, $b)
62 | ->addEdge($a, $c)
63 | ->addEdge($a, $d)
64 | ->addEdge($b, $h)
65 | ->addEdge($b, $e)
66 | ->addEdge($b, $f)
67 | ->addEdge($c, $f)
68 | ->addEdge($c, $g)
69 | ->addEdge($d, $g)
70 | ->addEdge($h, $i)
71 | ->addEdge($e, $i)
72 | ->addEdge($f, $i)
73 | ->addEdge($g, $i);
74 | \Swoole\Coroutine\run(function () use ($dag) {
75 | $dag->run();
76 | });
77 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 | ./tests/
14 |
15 |
--------------------------------------------------------------------------------
/src/Dag.php:
--------------------------------------------------------------------------------
1 |
23 | */
24 | protected $vertexes = [];
25 |
26 | /**
27 | * @var int
28 | */
29 | protected $concurrency = 10;
30 |
31 | /**
32 | * Add a vertex to the dag.
33 | * It doesn't make sense to add a vertex with the same key more than once.
34 | * If so they are simply ignored.
35 | */
36 | public function addVertex(Vertex $vertex): self
37 | {
38 | $this->vertexes[$vertex->key] = $vertex;
39 | return $this;
40 | }
41 |
42 | /**
43 | * Add an edge to the DAG.
44 | */
45 | public function addEdge(Vertex $from, Vertex $to): self
46 | {
47 | $from->children[] = $to;
48 | $to->parents[] = $from;
49 | return $this;
50 | }
51 |
52 | /**
53 | * Run the DAG.
54 | * @param array $args while using the nested dag, $args contains results from the parent dag.
55 | * in other cases, args can be used to modify dag behavior at run time.
56 | */
57 | public function run(array $args = []): array
58 | {
59 | $queue = new Channel(1);
60 | Coroutine::create(function () use ($queue) {
61 | $this->buildInitialQueue($queue);
62 | });
63 |
64 | $total = count($this->vertexes);
65 | $visited = [];
66 | $results = $args;
67 | $concurrent = new Concurrent($this->concurrency);
68 |
69 | while (count($visited) < $total) {
70 | $element = $queue->pop();
71 | if ($element instanceof \Throwable) {
72 | throw $element;
73 | }
74 | if (isset($visited[$element->key])) {
75 | continue;
76 | }
77 | // this channel will be closed after the completion of the corresponding task.
78 | $visited[$element->key] = new Channel();
79 | $concurrent->create(function () use ($queue, $visited, $element, &$results) {
80 | try {
81 | $results[$element->key] = call($element->value, [$results]);
82 | } catch (\Throwable $e) {
83 | $queue->push($e);
84 | throw $e;
85 | }
86 | $visited[$element->key]->close();
87 | if (empty($element->children)) {
88 | return;
89 | }
90 | Coroutine::create(function () use ($element, $queue, $visited) {
91 | $this->scheduleChildren($element, $queue, $visited);
92 | });
93 | });
94 | }
95 | // wait for all pending tasks to resolve
96 | foreach ($visited as $element) {
97 | $element->pop();
98 | }
99 | return $results;
100 | }
101 |
102 | public function getConcurrency(): int
103 | {
104 | return $this->concurrency;
105 | }
106 |
107 | public function setConcurrency(int $concurrency): self
108 | {
109 | $this->concurrency = $concurrency;
110 | return $this;
111 | }
112 |
113 | private function scheduleChildren(Vertex $element, Channel $queue, array $visited): void
114 | {
115 | foreach ($element->children as $child) {
116 | // Only schedule child if all parents but this one is complete
117 | foreach ($child->parents as $parent) {
118 | if ($parent->key == $element->key) {
119 | continue;
120 | }
121 | if (! isset($visited[$parent->key])) {
122 | continue 2;
123 | }
124 | // Parent might be running. Wait until completion.
125 | $visited[$parent->key]->pop();
126 | }
127 | $queue->push($child);
128 | }
129 | }
130 |
131 | private function buildInitialQueue(Channel $queue): void
132 | {
133 | $roots = [];
134 | /** @var Vertex $vertex */
135 | foreach ($this->vertexes as $vertex) {
136 | if (empty($vertex->parents)) {
137 | $roots[] = $vertex;
138 | }
139 | }
140 |
141 | if (empty($roots)) {
142 | throw new InvalidArgumentException('no roots can be found in dag');
143 | }
144 |
145 | foreach ($roots as $root) {
146 | $queue->push($root);
147 | }
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/src/Exception/InvalidArgumentException.php:
--------------------------------------------------------------------------------
1 |
28 | */
29 | public $parents = [];
30 |
31 | /**
32 | * @var array
33 | */
34 | public $children = [];
35 |
36 | public static function make(callable $job, string $key = null): self
37 | {
38 | $closure = \Closure::fromCallable($job);
39 | if ($key === null) {
40 | $key = spl_object_hash($closure);
41 | }
42 |
43 | $v = new Vertex();
44 | $v->key = $key;
45 | $v->value = $closure;
46 | return $v;
47 | }
48 |
49 | public static function of(Runner $job, string $key = null): self
50 | {
51 | if ($key === null) {
52 | $key = spl_object_hash($job);
53 | }
54 |
55 | $v = new Vertex();
56 | $v->key = $key;
57 | $v->value = [$job, 'run'];
58 | return $v;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/tests/Cases/DagTest.php:
--------------------------------------------------------------------------------
1 | push('A');
34 | });
35 | $b = \Hyperf\Dag\Vertex::make(function () use ($chan) {
36 | $chan->push('B');
37 | });
38 | $c = \Hyperf\Dag\Vertex::make(function () use ($chan) {
39 | $chan->push('C');
40 | });
41 | $d = \Hyperf\Dag\Vertex::make(function () use ($chan) {
42 | $chan->push('D');
43 | });
44 | $e = \Hyperf\Dag\Vertex::make(function () use ($chan) {
45 | $chan->push('E');
46 | });
47 | $f = \Hyperf\Dag\Vertex::make(function () use ($chan) {
48 | $chan->push('F');
49 | });
50 | $g = \Hyperf\Dag\Vertex::make(function () use ($chan) {
51 | $chan->push('G');
52 | });
53 | $h = \Hyperf\Dag\Vertex::make(function () use ($chan) {
54 | $chan->push('H');
55 | });
56 | $i = \Hyperf\Dag\Vertex::make(function () use ($chan) {
57 | $chan->push('I');
58 | });
59 | $dag->addVertex($a)
60 | ->addVertex($b)
61 | ->addVertex($c)
62 | ->addVertex($d)
63 | ->addVertex($e)
64 | ->addVertex($f)
65 | ->addVertex($g)
66 | ->addVertex($h)
67 | ->addVertex($i)
68 | ->addEdge($a, $i)
69 | ->addEdge($a, $i)
70 | ->addEdge($a, $b)
71 | ->addEdge($a, $c)
72 | ->addEdge($a, $d)
73 | ->addEdge($b, $h)
74 | ->addEdge($b, $e)
75 | ->addEdge($b, $f)
76 | ->addEdge($c, $f)
77 | ->addEdge($c, $g)
78 | ->addEdge($d, $g)
79 | ->addEdge($e, $i)
80 | ->addEdge($f, $i)
81 | ->addEdge($h, $i)
82 | ->addEdge($g, $i);
83 | Coroutine::create(function () use ($dag) {
84 | $dag->run();
85 | });
86 |
87 | $expected = ['A', 'B', 'C', 'D', 'H', 'E', 'F', 'G', 'I'];
88 | foreach ($expected as $e) {
89 | $data = $chan->pop();
90 | $this->assertEquals($e, $data);
91 | }
92 | }
93 |
94 | public function testAccessResults()
95 | {
96 | $a = Vertex::make(function () {
97 | return 1;
98 | }, 'a');
99 | $b = Vertex::make(function ($results) use ($a) {
100 | return $results[$a->key] + 1;
101 | }, 'b');
102 | $dag = new Dag();
103 | $dag->addVertex($a)->addVertex($b)->addEdge($a, $b);
104 | $result = $dag->run();
105 | $this->assertEquals(1, $result['a']);
106 | $this->assertEquals(2, $result['b']);
107 |
108 | $parent = new Dag();
109 | $results = $parent->addVertex(Vertex::of($dag, 'nest'))->run();
110 | $this->assertEquals(['a' => 1, 'b' => 2], $results['nest']);
111 | }
112 |
113 | public function testRunWithRace()
114 | {
115 | $fastChan = new Channel(1);
116 | $slowChan = new Channel(2);
117 | $a = Vertex::make(function () use ($fastChan) {
118 | $fastChan->push(0);
119 | });
120 | $b = Vertex::make(function () use ($fastChan) {
121 | $fastChan->push(1);
122 | });
123 | $c = Vertex::make(function () use ($fastChan) {
124 | $fastChan->push(2);
125 | });
126 | $d = Vertex::make(function () use ($slowChan) {
127 | $slowChan->push(3);
128 | });
129 | $dag = new Dag();
130 | $dag->addVertex($a)
131 | ->addVertex($b)
132 | ->addVertex($c)
133 | ->addVertex($d)
134 | ->addEdge($a, $b)
135 | ->addEdge($b, $c)
136 | ->addEdge($a, $d);
137 | Coroutine::create(function () use ($dag) {
138 | $dag->run();
139 | });
140 | $data = $fastChan->pop();
141 | $this->assertEquals(0, $data);
142 | $data = $fastChan->pop();
143 | $this->assertEquals(1, $data);
144 | $data = $fastChan->pop();
145 | $this->assertEquals(2, $data);
146 | $data = $slowChan->pop();
147 | $this->assertEquals(3, $data);
148 | }
149 |
150 | public function testRun()
151 | {
152 | $chan = new Channel(1);
153 | $a = Vertex::make(function () use ($chan) {
154 | $chan->push(0);
155 | });
156 | $b = Vertex::make(function () use ($chan) {
157 | $chan->push(0);
158 | });
159 | $c = Vertex::make(function () use ($chan) {
160 | $chan->push(1);
161 | });
162 | $d = Vertex::make(function () use ($chan) {
163 | $chan->push(1);
164 | });
165 | $dag = new Dag();
166 | $dag->addVertex($a)
167 | ->addVertex($b)
168 | ->addVertex($c)
169 | ->addVertex($d)
170 | ->addEdge($c, $a)
171 | ->addEdge($d, $b);
172 | Coroutine::create(function () use ($dag) {
173 | $dag->run();
174 | });
175 | $data = $chan->pop();
176 | $this->assertEquals(1, $data);
177 | $data = $chan->pop();
178 | $this->assertEquals(1, $data);
179 | $data = $chan->pop();
180 | $this->assertEquals(0, $data);
181 | $data = $chan->pop();
182 | $this->assertEquals(0, $data);
183 |
184 | $a = Vertex::make(function () use ($chan) {
185 | $chan->push(0);
186 | });
187 | $b = Vertex::make(function () use ($chan) {
188 | $chan->push(1);
189 | });
190 | $c = Vertex::make(function () use ($chan) {
191 | $chan->push(1);
192 | });
193 | $d = Vertex::make(function () use ($chan) {
194 | $chan->push(1);
195 | });
196 | $dag = new Dag();
197 | $dag->addVertex($a)
198 | ->addVertex($b)
199 | ->addVertex($c)
200 | ->addVertex($d)
201 | ->addEdge($c, $a)
202 | ->addEdge($d, $a)
203 | ->addEdge($b, $a);
204 | Coroutine::create(function () use ($dag) {
205 | $dag->run();
206 | });
207 | $data = $chan->pop();
208 | $this->assertEquals(1, $data);
209 | $data = $chan->pop();
210 | $this->assertEquals(1, $data);
211 | $data = $chan->pop();
212 | $this->assertEquals(1, $data);
213 | $data = $chan->pop();
214 | $this->assertEquals(0, $data);
215 |
216 | $e = Vertex::make(function () use ($chan) {
217 | $chan->push(2);
218 | });
219 | $f = Vertex::of($dag);
220 | $nestedDag = new Dag();
221 | $nestedDag->addVertex($e)->addVertex($f)->addEdge($e, $f);
222 | Coroutine::create(function () use ($nestedDag) {
223 | $nestedDag->run();
224 | });
225 | $data = $chan->pop();
226 | $this->assertEquals(2, $data);
227 | $data = $chan->pop();
228 | $this->assertEquals(1, $data);
229 | $data = $chan->pop();
230 | $this->assertEquals(1, $data);
231 | $data = $chan->pop();
232 | $this->assertEquals(1, $data);
233 | $data = $chan->pop();
234 | $this->assertEquals(0, $data);
235 | }
236 |
237 | public function testException()
238 | {
239 | $dag = new Dag();
240 | $chan = new Channel(1);
241 | $a = \Hyperf\Dag\Vertex::make(function () {
242 | throw new \Exception('should abort dag');
243 | });
244 | $b = \Hyperf\Dag\Vertex::make(function () {
245 | $this->assertFalse(true, 'should not reach here');
246 | });
247 | $c = \Hyperf\Dag\Vertex::make(function () {
248 | $this->assertTrue(true);
249 | });
250 | $dag->addVertex($a)->addVertex($b)->addVertex($c)->addEdge($a, $b)->addVertex($c, $b);
251 | $this->expectException(\Exception::class);
252 | $dag->run();
253 | }
254 | }
255 |
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 |