├── .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.ink/img/eyJjb2RlIjoic3RhdGVEaWFncmFtLXYyXG4gICAgWypdIC0tPiBBXG4gICAgQSAtLT4gQlxuICAgIEEgLS0-IENcbiAgICBBIC0tPiBEXG4gICAgRCAtLT4gR1xuICAgIEMgLS0-IEdcbiAgICBDIC0tPiBGXG4gICAgQiAtLT4gRlxuICAgIEIgLS0-IEVcbiAgICBCIC0tPiBIXG4gICAgSCAtLT4gSVxuICAgIEUgLS0-IElcbiAgICBGIC0tPiBJXG4gICAgRyAtLT4gSVxuICAgIEkgLS0-IFsqXVxuICAgICAgICAgICAgIiwibWVybWFpZCI6eyJ0aGVtZSI6ImRlZmF1bHQiLCJ0aGVtZVZhcmlhYmxlcyI6eyJiYWNrZ3JvdW5kIjoid2hpdGUiLCJwcmltYXJ5Q29sb3IiOiIjRUNFQ0ZGIiwic2Vjb25kYXJ5Q29sb3IiOiIjZmZmZmRlIiwidGVydGlhcnlDb2xvciI6ImhzbCg4MCwgMTAwJSwgOTYuMjc0NTA5ODAzOSUpIiwicHJpbWFyeUJvcmRlckNvbG9yIjoiaHNsKDI0MCwgNjAlLCA4Ni4yNzQ1MDk4MDM5JSkiLCJzZWNvbmRhcnlCb3JkZXJDb2xvciI6ImhzbCg2MCwgNjAlLCA4My41Mjk0MTE3NjQ3JSkiLCJ0ZXJ0aWFyeUJvcmRlckNvbG9yIjoiaHNsKDgwLCA2MCUsIDg2LjI3NDUwOTgwMzklKSIsInByaW1hcnlUZXh0Q29sb3IiOiIjMTMxMzAwIiwic2Vjb25kYXJ5VGV4dENvbG9yIjoiIzAwMDAyMSIsInRlcnRpYXJ5VGV4dENvbG9yIjoicmdiKDkuNTAwMDAwMDAwMSwgOS41MDAwMDAwMDAxLCA5LjUwMDAwMDAwMDEpIiwibGluZUNvbG9yIjoiIzMzMzMzMyIsInRleHRDb2xvciI6IiMzMzMiLCJtYWluQmtnIjoiI0VDRUNGRiIsInNlY29uZEJrZyI6IiNmZmZmZGUiLCJib3JkZXIxIjoiIzkzNzBEQiIsImJvcmRlcjIiOiIjYWFhYTMzIiwiYXJyb3doZWFkQ29sb3IiOiIjMzMzMzMzIiwiZm9udEZhbWlseSI6IlwidHJlYnVjaGV0IG1zXCIsIHZlcmRhbmEsIGFyaWFsIiwiZm9udFNpemUiOiIxNnB4IiwibGFiZWxCYWNrZ3JvdW5kIjoiI2U4ZThlOCIsIm5vZGVCa2ciOiIjRUNFQ0ZGIiwibm9kZUJvcmRlciI6IiM5MzcwREIiLCJjbHVzdGVyQmtnIjoiI2ZmZmZkZSIsImNsdXN0ZXJCb3JkZXIiOiIjYWFhYTMzIiwiZGVmYXVsdExpbmtDb2xvciI6IiMzMzMzMzMiLCJ0aXRsZUNvbG9yIjoiIzMzMyIsImVkZ2VMYWJlbEJhY2tncm91bmQiOiIjZThlOGU4IiwiYWN0b3JCb3JkZXIiOiJoc2woMjU5LjYyNjE2ODIyNDMsIDU5Ljc3NjUzNjMxMjglLCA4Ny45MDE5NjA3ODQzJSkiLCJhY3RvckJrZyI6IiNFQ0VDRkYiLCJhY3RvclRleHRDb2xvciI6ImJsYWNrIiwiYWN0b3JMaW5lQ29sb3IiOiJncmV5Iiwic2lnbmFsQ29sb3IiOiIjMzMzIiwic2lnbmFsVGV4dENvbG9yIjoiIzMzMyIsImxhYmVsQm94QmtnQ29sb3IiOiIjRUNFQ0ZGIiwibGFiZWxCb3hCb3JkZXJDb2xvciI6ImhzbCgyNTkuNjI2MTY4MjI0MywgNTkuNzc2NTM2MzEyOCUsIDg3LjkwMTk2MDc4NDMlKSIsImxhYmVsVGV4dENvbG9yIjoiYmxhY2siLCJsb29wVGV4dENvbG9yIjoiYmxhY2siLCJub3RlQm9yZGVyQ29sb3IiOiIjYWFhYTMzIiwibm90ZUJrZ0NvbG9yIjoiI2ZmZjVhZCIsIm5vdGVUZXh0Q29sb3IiOiJibGFjayIsImFjdGl2YXRpb25Cb3JkZXJDb2xvciI6IiM2NjYiLCJhY3RpdmF0aW9uQmtnQ29sb3IiOiIjZjRmNGY0Iiwic2VxdWVuY2VOdW1iZXJDb2xvciI6IndoaXRlIiwic2VjdGlvbkJrZ0NvbG9yIjoicmdiYSgxMDIsIDEwMiwgMjU1LCAwLjQ5KSIsImFsdFNlY3Rpb25Ca2dDb2xvciI6IndoaXRlIiwic2VjdGlvbkJrZ0NvbG9yMiI6IiNmZmY0MDAiLCJ0YXNrQm9yZGVyQ29sb3IiOiIjNTM0ZmJjIiwidGFza0JrZ0NvbG9yIjoiIzhhOTBkZCIsInRhc2tUZXh0TGlnaHRDb2xvciI6IndoaXRlIiwidGFza1RleHRDb2xvciI6IndoaXRlIiwidGFza1RleHREYXJrQ29sb3IiOiJibGFjayIsInRhc2tUZXh0T3V0c2lkZUNvbG9yIjoiYmxhY2siLCJ0YXNrVGV4dENsaWNrYWJsZUNvbG9yIjoiIzAwMzE2MyIsImFjdGl2ZVRhc2tCb3JkZXJDb2xvciI6IiM1MzRmYmMiLCJhY3RpdmVUYXNrQmtnQ29sb3IiOiIjYmZjN2ZmIiwiZ3JpZENvbG9yIjoibGlnaHRncmV5IiwiZG9uZVRhc2tCa2dDb2xvciI6ImxpZ2h0Z3JleSIsImRvbmVUYXNrQm9yZGVyQ29sb3IiOiJncmV5IiwiY3JpdEJvcmRlckNvbG9yIjoiI2ZmODg4OCIsImNyaXRCa2dDb2xvciI6InJlZCIsInRvZGF5TGluZUNvbG9yIjoicmVkIiwibGFiZWxDb2xvciI6ImJsYWNrIiwiZXJyb3JCa2dDb2xvciI6IiM1NTIyMjIiLCJlcnJvclRleHRDb2xvciI6IiM1NTIyMjIiLCJjbGFzc1RleHQiOiIjMTMxMzAwIiwiZmlsbFR5cGUwIjoiI0VDRUNGRiIsImZpbGxUeXBlMSI6IiNmZmZmZGUiLCJmaWxsVHlwZTIiOiJoc2woMzA0LCAxMDAlLCA5Ni4yNzQ1MDk4MDM5JSkiLCJmaWxsVHlwZTMiOiJoc2woMTI0LCAxMDAlLCA5My41Mjk0MTE3NjQ3JSkiLCJmaWxsVHlwZTQiOiJoc2woMTc2LCAxMDAlLCA5Ni4yNzQ1MDk4MDM5JSkiLCJmaWxsVHlwZTUiOiJoc2woLTQsIDEwMCUsIDkzLjUyOTQxMTc2NDclKSIsImZpbGxUeXBlNiI6ImhzbCg4LCAxMDAlLCA5Ni4yNzQ1MDk4MDM5JSkiLCJmaWxsVHlwZTciOiJoc2woMTg4LCAxMDAlLCA5My41Mjk0MTE3NjQ3JSkifX0sInVwZGF0ZUVkaXRvciI6ZmFsc2V9)](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 |