From eff2f7ce8d19d6da55a1794493c3dc44aad8ac0c Mon Sep 17 00:00:00 2001 From: QIUZHILEI <2925212608@qq.com> Date: Sun, 25 Jun 2023 10:46:00 +0800 Subject: [PATCH] dagrs update Signed-off-by: QIUZHILEI <2925212608@qq.com> --- dagrs/.github/workflows/base.yml | 72 +++ dagrs/.gitignore | 16 +- dagrs/Cargo.toml | 14 +- dagrs/LICENSE | 127 ----- dagrs/LICENSE-APACHE | 201 ++++++++ dagrs/LICENSE-MIT | 21 + dagrs/LICENSE-THIRD-PARTY | 0 dagrs/README.md | 471 +++++++++--------- dagrs/assets/execute_logic.png | Bin 0 -> 56356 bytes dagrs/assets/loop.png | Bin 0 -> 14021 bytes dagrs/assets/tasks.png | Bin 0 -> 16028 bytes dagrs/examples/compute_dag.rs | 67 +++ dagrs/examples/custom_log.rs | 63 +++ dagrs/examples/custom_parser.rs | 155 ++++++ dagrs/examples/custom_task.rs | 92 ++++ dagrs/examples/engine.rs | 89 ++++ dagrs/examples/hello.rs | 38 -- dagrs/examples/hello_env.rs | 39 -- dagrs/examples/hello_script.rs | 26 - dagrs/examples/impl_action.rs | 49 ++ dagrs/examples/use_macro.rs | 56 +++ dagrs/examples/yaml_dag.rs | 11 + dagrs/src/engine/dag.rs | 337 +++++++++++++ dagrs/src/engine/dag_engine.rs | 211 -------- dagrs/src/engine/env_variables.rs | 59 --- dagrs/src/engine/error.rs | 38 ++ dagrs/src/engine/error_handler.rs | 76 --- dagrs/src/engine/graph.rs | 131 ++--- dagrs/src/engine/mod.rs | 102 +++- dagrs/src/lib.rs | 152 +----- dagrs/src/main.rs | 180 ++----- dagrs/src/parser/error.rs | 65 +++ dagrs/src/parser/mod.rs | 81 +++ dagrs/src/parser/yaml_parser.rs | 125 +++++ dagrs/src/task/error.rs | 72 +++ dagrs/src/task/mod.rs | 285 ++++++++++- dagrs/src/task/script.rs | 92 ++++ dagrs/src/task/specific_task.rs | 70 +++ dagrs/src/task/state.rs | 177 ++++--- dagrs/src/task/task.rs | 234 --------- dagrs/src/task/yaml_task.rs | 194 -------- dagrs/src/utils/env.rs | 54 ++ dagrs/src/utils/gen_macro.rs | 25 + dagrs/src/utils/log.rs | 203 ++++++++ dagrs/src/utils/mod.rs | 14 + dagrs/test/test.sh | 1 - dagrs/test/test_dag1.yaml | 12 - dagrs/test/test_error2.yaml | 6 - dagrs/test/test_loop2.yaml | 49 -- dagrs/test/test_value_pass1.txt | 1 - dagrs/test/test_value_pass1.yaml | 13 - dagrs/test/test_value_pass2.yaml | 13 - .../config/correct.yaml} | 32 +- dagrs/tests/config/custom_file_task.txt | 8 + dagrs/tests/config/empty_file.yaml | 1 + dagrs/tests/config/illegal_content.yaml | 6 + .../config/loop_error.yaml} | 20 +- .../config/no_run.yaml} | 2 +- dagrs/tests/config/no_script.yaml | 6 + dagrs/tests/config/no_start_with_dagrs.yaml | 47 ++ dagrs/tests/config/no_task_name.yaml | 5 + dagrs/tests/config/no_type.yaml | 4 + .../config/precursor_not_found.yaml} | 4 +- dagrs/tests/config/script_run_failed.yaml | 48 ++ .../config/self_loop_error.yaml} | 9 +- dagrs/tests/config/unsupported_type.yaml | 5 + dagrs/tests/dag_job_test.rs | 126 +++++ dagrs/tests/env_test.rs | 44 ++ dagrs/tests/yaml_parser_test.rs | 72 +++ 69 files changed, 3296 insertions(+), 1822 deletions(-) create mode 100644 dagrs/.github/workflows/base.yml delete mode 100644 dagrs/LICENSE create mode 100644 dagrs/LICENSE-APACHE create mode 100644 dagrs/LICENSE-MIT create mode 100644 dagrs/LICENSE-THIRD-PARTY create mode 100644 dagrs/assets/execute_logic.png create mode 100644 dagrs/assets/loop.png create mode 100644 dagrs/assets/tasks.png create mode 100644 dagrs/examples/compute_dag.rs create mode 100644 dagrs/examples/custom_log.rs create mode 100644 dagrs/examples/custom_parser.rs create mode 100644 dagrs/examples/custom_task.rs create mode 100644 dagrs/examples/engine.rs delete mode 100644 dagrs/examples/hello.rs delete mode 100644 dagrs/examples/hello_env.rs delete mode 100644 dagrs/examples/hello_script.rs create mode 100644 dagrs/examples/impl_action.rs create mode 100644 dagrs/examples/use_macro.rs create mode 100644 dagrs/examples/yaml_dag.rs create mode 100644 dagrs/src/engine/dag.rs delete mode 100644 dagrs/src/engine/dag_engine.rs delete mode 100644 dagrs/src/engine/env_variables.rs create mode 100644 dagrs/src/engine/error.rs delete mode 100644 dagrs/src/engine/error_handler.rs create mode 100644 dagrs/src/parser/error.rs create mode 100644 dagrs/src/parser/mod.rs create mode 100644 dagrs/src/parser/yaml_parser.rs create mode 100644 dagrs/src/task/error.rs create mode 100644 dagrs/src/task/script.rs create mode 100644 dagrs/src/task/specific_task.rs delete mode 100644 dagrs/src/task/task.rs delete mode 100644 dagrs/src/task/yaml_task.rs create mode 100644 dagrs/src/utils/env.rs create mode 100644 dagrs/src/utils/gen_macro.rs create mode 100644 dagrs/src/utils/log.rs create mode 100644 dagrs/src/utils/mod.rs delete mode 100755 dagrs/test/test.sh delete mode 100644 dagrs/test/test_dag1.yaml delete mode 100644 dagrs/test/test_error2.yaml delete mode 100644 dagrs/test/test_loop2.yaml delete mode 100644 dagrs/test/test_value_pass1.txt delete mode 100644 dagrs/test/test_value_pass1.yaml delete mode 100644 dagrs/test/test_value_pass2.yaml rename dagrs/{test/test_dag2.yaml => tests/config/correct.yaml} (57%) create mode 100644 dagrs/tests/config/custom_file_task.txt create mode 100644 dagrs/tests/config/empty_file.yaml create mode 100644 dagrs/tests/config/illegal_content.yaml rename dagrs/{test/test_loop1.yaml => tests/config/loop_error.yaml} (58%) rename dagrs/{test/test_error4.yaml => tests/config/no_run.yaml} (38%) create mode 100644 dagrs/tests/config/no_script.yaml create mode 100644 dagrs/tests/config/no_start_with_dagrs.yaml create mode 100644 dagrs/tests/config/no_task_name.yaml create mode 100644 dagrs/tests/config/no_type.yaml rename dagrs/{test/test_error3.yaml => tests/config/precursor_not_found.yaml} (60%) create mode 100644 dagrs/tests/config/script_run_failed.yaml rename dagrs/{test/test_error1.yaml => tests/config/self_loop_error.yaml} (36%) create mode 100644 dagrs/tests/config/unsupported_type.yaml create mode 100644 dagrs/tests/dag_job_test.rs create mode 100644 dagrs/tests/env_test.rs create mode 100644 dagrs/tests/yaml_parser_test.rs diff --git a/dagrs/.github/workflows/base.yml b/dagrs/.github/workflows/base.yml new file mode 100644 index 00000000..d38f8738 --- /dev/null +++ b/dagrs/.github/workflows/base.yml @@ -0,0 +1,72 @@ +# Based on https://github.com/actions-rs/meta/blob/master/recipes/quickstart.md +# +# +# +# + +on: [push, pull_request] + +name: Base + +jobs: + check: + name: Check + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v2 + + - name: Install stable toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + + - name: Run cargo check + uses: actions-rs/cargo@v1 + continue-on-error: false + with: + command: check + + test: + name: Test Suite + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v2 + + - name: Install stable toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + + - name: Run cargo test + uses: actions-rs/cargo@v1 + continue-on-error: false + with: + command: test + + lints: + name: Lints + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v2 + + - name: Install stable toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + components: rustfmt, clippy + + - name: Run cargo clippy + uses: actions-rs/cargo@v1 + continue-on-error: true + with: + command: clippy + args: -- -D warnings \ No newline at end of file diff --git a/dagrs/.gitignore b/dagrs/.gitignore index 293a004d..9c9f7dcd 100644 --- a/dagrs/.gitignore +++ b/dagrs/.gitignore @@ -9,14 +9,12 @@ Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk - -#Added by cargo -# -#already existing elements are commented out - -/target -#**/*.rs.bk -dagrs.log - # IDE .idea +.vscode + +# Exclude test output result files +/test/*.txt + +# Exclude execute log +/*.log \ No newline at end of file diff --git a/dagrs/Cargo.toml b/dagrs/Cargo.toml index 3c3ddab1..1a94e230 100644 --- a/dagrs/Cargo.toml +++ b/dagrs/Cargo.toml @@ -7,13 +7,11 @@ edition = "2021" [dependencies] yaml-rust = "0.4.5" -lazy_static = "1.4.0" bimap = "0.6.1" -deno_core = "0.121.0" -log = "0.4.14" -simplelog = "0.12.0" -clap = { version = "3.0.14", features = ["derive"] } -anymap = "1.0.0-beta.2" -crossbeam = "0.8.1" +deno_core = "0.191.0" +clap = { version = "4.2.2", features = ["derive"] } +anymap2 = "0.13.0" thiserror = "1.0.30" -tokio = { version = "1.18", features = ["rt", "rt-multi-thread"] } \ No newline at end of file +tokio = { version = "1.28", features = ["rt", "sync","rt-multi-thread"] } +log = "0.4" +simplelog = "0.12" \ No newline at end of file diff --git a/dagrs/LICENSE b/dagrs/LICENSE deleted file mode 100644 index ee583996..00000000 --- a/dagrs/LICENSE +++ /dev/null @@ -1,127 +0,0 @@ - 木兰宽松许可证, 第2版 - - 木兰宽松许可证, 第2版 - 2020年1月 http://license.coscl.org.cn/MulanPSL2 - - - 您对“软件”的复制、使用、修改及分发受木兰宽松许可证,第2版(“本许可证”)的如下条款的约束: - - 0. 定义 - - “软件”是指由“贡献”构成的许可在“本许可证”下的程序和相关文档的集合。 - - “贡献”是指由任一“贡献者”许可在“本许可证”下的受版权法保护的作品。 - - “贡献者”是指将受版权法保护的作品许可在“本许可证”下的自然人或“法人实体”。 - - “法人实体”是指提交贡献的机构及其“关联实体”。 - - “关联实体”是指,对“本许可证”下的行为方而言,控制、受控制或与其共同受控制的机构,此处的控制是指有受控方或共同受控方至少50%直接或间接的投票权、资金或其他有价证券。 - - 1. 授予版权许可 - - 每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的版权许可,您可以复制、使用、修改、分发其“贡献”,不论修改与否。 - - 2. 授予专利许可 - - 每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的(根据本条规定撤销除外)专利许可,供您制造、委托制造、使用、许诺销售、销售、进口其“贡献”或以其他方式转移其“贡献”。前述专利许可仅限于“贡献者”现在或将来拥有或控制的其“贡献”本身或其“贡献”与许可“贡献”时的“软件”结合而将必然会侵犯的专利权利要求,不包括对“贡献”的修改或包含“贡献”的其他结合。如果您或您的“关联实体”直接或间接地,就“软件”或其中的“贡献”对任何人发起专利侵权诉讼(包括反诉或交叉诉讼)或其他专利维权行动,指控其侵犯专利权,则“本许可证”授予您对“软件”的专利许可自您提起诉讼或发起维权行动之日终止。 - - 3. 无商标许可 - - “本许可证”不提供对“贡献者”的商品名称、商标、服务标志或产品名称的商标许可,但您为满足第4条规定的声明义务而必须使用除外。 - - 4. 分发限制 - - 您可以在任何媒介中将“软件”以源程序形式或可执行形式重新分发,不论修改与否,但您必须向接收者提供“本许可证”的副本,并保留“软件”中的版权、商标、专利及免责声明。 - - 5. 免责声明与责任限制 - - “软件”及其中的“贡献”在提供时不带任何明示或默示的担保。在任何情况下,“贡献者”或版权所有者不对任何人因使用“软件”或其中的“贡献”而引发的任何直接或间接损失承担责任,不论因何种原因导致或者基于何种法律理论,即使其曾被建议有此种损失的可能性。 - - 6. 语言 - “本许可证”以中英文双语表述,中英文版本具有同等法律效力。如果中英文版本存在任何冲突不一致,以中文版为准。 - - 条款结束 - - 如何将木兰宽松许可证,第2版,应用到您的软件 - - 如果您希望将木兰宽松许可证,第2版,应用到您的新软件,为了方便接收者查阅,建议您完成如下三步: - - 1, 请您补充如下声明中的空白,包括软件名、软件的首次发表年份以及您作为版权人的名字; - - 2, 请您在软件包的一级目录下创建以“LICENSE”为名的文件,将整个许可证文本放入该文件中; - - 3, 请将如下声明文本放入每个源文件的头部注释中。 - - Copyright (c) [Year] [name of copyright holder] - [Software Name] is licensed under Mulan PSL v2. - You can use this software according to the terms and conditions of the Mulan PSL v2. - You may obtain a copy of Mulan PSL v2 at: - http://license.coscl.org.cn/MulanPSL2 - THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. - See the Mulan PSL v2 for more details. - - - Mulan Permissive Software License,Version 2 - - Mulan Permissive Software License,Version 2 (Mulan PSL v2) - January 2020 http://license.coscl.org.cn/MulanPSL2 - - Your reproduction, use, modification and distribution of the Software shall be subject to Mulan PSL v2 (this License) with the following terms and conditions: - - 0. Definition - - Software means the program and related documents which are licensed under this License and comprise all Contribution(s). - - Contribution means the copyrightable work licensed by a particular Contributor under this License. - - Contributor means the Individual or Legal Entity who licenses its copyrightable work under this License. - - Legal Entity means the entity making a Contribution and all its Affiliates. - - Affiliates means entities that control, are controlled by, or are under common control with the acting entity under this License, ‘control’ means direct or indirect ownership of at least fifty percent (50%) of the voting power, capital or other securities of controlled or commonly controlled entity. - - 1. Grant of Copyright License - - Subject to the terms and conditions of this License, each Contributor hereby grants to you a perpetual, worldwide, royalty-free, non-exclusive, irrevocable copyright license to reproduce, use, modify, or distribute its Contribution, with modification or not. - - 2. Grant of Patent License - - Subject to the terms and conditions of this License, each Contributor hereby grants to you a perpetual, worldwide, royalty-free, non-exclusive, irrevocable (except for revocation under this Section) patent license to make, have made, use, offer for sale, sell, import or otherwise transfer its Contribution, where such patent license is only limited to the patent claims owned or controlled by such Contributor now or in future which will be necessarily infringed by its Contribution alone, or by combination of the Contribution with the Software to which the Contribution was contributed. The patent license shall not apply to any modification of the Contribution, and any other combination which includes the Contribution. If you or your Affiliates directly or indirectly institute patent litigation (including a cross claim or counterclaim in a litigation) or other patent enforcement activities against any individual or entity by alleging that the Software or any Contribution in it infringes patents, then any patent license granted to you under this License for the Software shall terminate as of the date such litigation or activity is filed or taken. - - 3. No Trademark License - - No trademark license is granted to use the trade names, trademarks, service marks, or product names of Contributor, except as required to fulfill notice requirements in Section 4. - - 4. Distribution Restriction - - You may distribute the Software in any medium with or without modification, whether in source or executable forms, provided that you provide recipients with a copy of this License and retain copyright, patent, trademark and disclaimer statements in the Software. - - 5. Disclaimer of Warranty and Limitation of Liability - - THE SOFTWARE AND CONTRIBUTION IN IT ARE PROVIDED WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED. IN NO EVENT SHALL ANY CONTRIBUTOR OR COPYRIGHT HOLDER BE LIABLE TO YOU FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO ANY DIRECT, OR INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING FROM YOUR USE OR INABILITY TO USE THE SOFTWARE OR THE CONTRIBUTION IN IT, NO MATTER HOW IT’S CAUSED OR BASED ON WHICH LEGAL THEORY, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. - - 6. Language - - THIS LICENSE IS WRITTEN IN BOTH CHINESE AND ENGLISH, AND THE CHINESE VERSION AND ENGLISH VERSION SHALL HAVE THE SAME LEGAL EFFECT. IN THE CASE OF DIVERGENCE BETWEEN THE CHINESE AND ENGLISH VERSIONS, THE CHINESE VERSION SHALL PREVAIL. - - END OF THE TERMS AND CONDITIONS - - How to Apply the Mulan Permissive Software License,Version 2 (Mulan PSL v2) to Your Software - - To apply the Mulan PSL v2 to your work, for easy identification by recipients, you are suggested to complete following three steps: - - i Fill in the blanks in following statement, including insert your software name, the year of the first publication of your software, and your name identified as the copyright owner; - - ii Create a file named “LICENSE” which contains the whole context of this License in the first directory of your software package; - - iii Attach the statement to the appropriate annotated syntax at the beginning of each source file. - - - Copyright (c) [Year] [name of copyright holder] - [Software Name] is licensed under Mulan PSL v2. - You can use this software according to the terms and conditions of the Mulan PSL v2. - You may obtain a copy of Mulan PSL v2 at: - http://license.coscl.org.cn/MulanPSL2 - THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. - See the Mulan PSL v2 for more details. diff --git a/dagrs/LICENSE-APACHE b/dagrs/LICENSE-APACHE new file mode 100644 index 00000000..b651cf1c --- /dev/null +++ b/dagrs/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/LICENSE-2.0 + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2022 Open Rust Initiative + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/dagrs/LICENSE-MIT b/dagrs/LICENSE-MIT new file mode 100644 index 00000000..91127361 --- /dev/null +++ b/dagrs/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Open Rust Initiative + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/dagrs/LICENSE-THIRD-PARTY b/dagrs/LICENSE-THIRD-PARTY new file mode 100644 index 00000000..e69de29b diff --git a/dagrs/README.md b/dagrs/README.md index 4a2c064b..30ee2258 100644 --- a/dagrs/README.md +++ b/dagrs/README.md @@ -1,278 +1,293 @@ # dagrs -本项目是用 Rust 写的 DAG 执行引擎,开发文档请参考:[使用 Rust 编写 DAG 执行引擎](https://openeuler.feishu.cn/docs/doccnVLprAY6vIMv6W1vgfLnfrf)。 +## What is dagrs + +dagrs are suitable for the execution of multiple tasks with graph-like dependencies. dagrs has the characteristics of high performance and asynchronous execution. It provides users with a convenient programming interface. + +## What can dagrs do + +dagrs allows users to easily execute multiple sets of tasks with complex graph dependencies. It only requires: +The user defines tasks and specifies the dependencies of the tasks, and dagrs can execute the tasks sequentially in the topological sequence of the graph. +For example: + +![image-20230508154216925](assets/tasks.png) + +This graph represents the dependencies between tasks, and the graph composed of tasks must satisfy two points: + +- A graph allows only one point with zero in-degree and zero out-degree(Only one start task and one end task are allowed). + +- The graph itself is directed, and the user must ensure that there are no loops in the graph, that is, the dependencies of tasks cannot form a closed loop, otherwise the engine will refuse to execute all tasks, for example: + + ![image-20230508154555260](assets/loop.png) + +Among them, each task may produce output, and may also require the output of some tasks as its input. + +## The way to use dagrs + +dagrs provides users with two basic task execution methods: + +1. The first one: the user does not need to program, but only needs to provide the task configuration file in yaml format. A standard yaml configuration file format is given below: + + ```yaml + dagrs: + a: + name: "Task 1" + after: [ b, c ] + run: + type: sh + script: echo a + b: + name: "Task 2" + after: [ c, f, g ] + run: + type: sh + script: echo b + c: + name: "Task 3" + after: [ e, g ] + run: + type: sh + script: echo c + d: + name: "Task 4" + after: [ c, e ] + run: + type: sh + script: echo d + e: + name: "Task 5" + after: [ h ] + run: + type: sh + script: echo e + f: + name: "Task 6" + after: [ g ] + run: + type: deno + script: Deno.core.print("f\n") + g: + name: "Task 7" + after: [ h ] + run: + type: deno + script: Deno.core.print("g\n") + h: + name: "Task 8" + run: + type: sh + script: echo h + ``` + + These yaml-defined task items form a complex dependency graph. In the yaml configuration file: + + - The file starts with `dagrs` + - Similar to `a`, `b`, `c`... is the unique identifier of the task + - `name` is a required attribute, which is the name of the task + - `after` is an optional attribute (only the first executed task does not have this attribute), which represents which tasks are executed after the task, that is, specifies dependencies for tasks + - `run` is a required attribute, followed by `type` and `script`, they are all required attributes, where `type` represents the type of task. Two types of tasks are currently supported: one is sh script and the other is JavaScript script. `script` represents the command to be executed + + To execute the yaml configured file, you need to compile this project, requiring rust version >= 1.70: + + ```bash + $cargo build --release + $./target/release/dagrs --yaml=./tests/config/correct.yaml --log-path=./dagrs.log --log-level=info + ``` + + You can see an example: `examples/yaml_dag.rs` + +2. The second way is to programmatically implement the Action interface to rewrite the run function and construct a series of `DefaultTasks`. The example: `examples/compute_dag.rs`. `DefaultTask` is the default implementation of the Task trait, and it has several mandatory attributes: + + - `id`: uniquely identifies the task assigned by the global ID assigner + - `name`: the name of the task + - `predecessor_tasks`: the predecessor tasks of this task + - `action`: is a dynamic type that implements the Action trait in user programming, and it is the specific logic to be executed by the task + +**In addition to these two methods, dagrs also supports advanced task custom configuration.** + +- `DefaultTask` is a default implementation of the `Task` trait. Users can also customize tasks and add more functions and attributes to tasks, but they still need to have the four necessary attributes in `DefaultTask`. `YamlTask` is another example of `Task` concrete implementation, its source code is available for reference, or refer to `example/custom_task.rs`. +- In addition to yaml-type configuration files, users can also provide other types of configuration files, but in order to allow other types of configuration files to be parsed as tasks, users need to implement the `Parser` trait. `YamlParser` source code is available for reference, or refer to `examples/custom_parser.rs` + +## Try it out + +Make sure the Rust compilation environment is available . + +### Programmatically implement task definition + +The way to use the yaml configuration file has been given above. Here we mainly discuss the way of programming to implement Action traits and provide Task. -## 用法 - -确保 Rust 编译环境可用(`cargo build`),然后在此文件夹中运行`cargo build --release`,在`target/release/`中获取可执行文件,并将其放入PATH。 - -本项目面向两个目标群体: - -- 普通用户 - 通过 `YAML` 文件对任务进行定义和调度运行。 -- 程序员 - 通过实现 `Task Trait` 进行任务的定义和调度运行。 - - -## YAML - -此部分是面向普通用户的,即用户并不通过 rust 编程,而是利用 YAML 文件对任务进行描述并调度运行。YAML 文件的一个示例如下: +```rust +use std::sync::Arc; +use dagrs::{ + Action, + Dag, DefaultTask, EnvVar,log, Input, Output, RunningError,LogLevel +}; + +struct SimpleAction(usize); +/// Implement the `Action` trait for `SimpleAction`, defining the logic of the `run` function. +/// The logic here is simply to get the output value (usize) of all predecessor tasks and then accumulate. +impl Action for SimpleAction{ + fn run(&self, input: Input,env:Arc) -> Result { + let base = env.get::("base").unwrap(); + let mut sum = self.0; + input + .get_iter() + .for_each(|i| sum += i.get::().unwrap() * base); + Ok(Output::new(sum)) + } +} -```yaml -dagrs: - a: - name: 任务1 - after: [b] - from: [b] - run: - type: sh - script: ./test/test.sh - b: - name: "任务2" - run: - type: deno - script: print("Hello!") +// Initialize the global logger +log::init_logger(LogLevel::Info,None); +// Generate four tasks. +let a= DefaultTask::new(SimpleAction(10),"Task a"); +let mut b=DefaultTask::new(SimpleAction(20),"Task b"); +let mut c=DefaultTask::new(SimpleAction(30),"Task c"); +let mut d=DefaultTask::new(SimpleAction(40),"Task d"); +// Set the precursor for each task. +b.set_predecessors(&[&a]); +c.set_predecessors(&[&a]); +d.set_predecessors(&[&b,&c]); +// Take these four tasks as a Dag. +let mut dag=Dag::with_tasks(vec![a,b,c,d]); +// Set a global environment variable for this dag. +let mut env = EnvVar::new(); +env.set("base", 2usize); +dag.set_env(env); +// Begin execution. +assert!(dag.start().unwrap()); +// Get execution result +assert_eq!(dag.get_result::().unwrap(),220); ``` -- YAML 文件应该以 `dagrs` 开头。 - -- `a,b` 是任务的标识符(也可理解为 ID),主要作为标识使用,无具体含义。该字段必须存在且不能重复(否则会覆盖早先定义)。 -- `name` 是任务的名称,在后续调度时会输出到 log 中以便用户查看。该字段必须存在,可以重复。 -- `after` 是任务的执行顺序,如 `after: [b]` 就表明 `a` 要在 `b` 之后执行。 -- `from` 是任务输入值的来源,`from: [b]` 表示 `a` 在开始执行时,会得到 `b` 的执行结果,以字符串的形式输入。 -- `run` 是任务的内容定义,包括 `type` 和 `script` 两个子字段。该字段及其子字段必须存在。 - - `type` 是任务的执行方式,当前支持 shell 执行(sh),和 deno 执行(deno)。 - - `script` 是任务的执行内容,可以是具体的命令,也可以是一个文件。 - - - -另一个实际涉及输入输出的例子: - -```yaml -dagrs: - a: - name: "任务1" - after: [b] - from: [b] - run: - type: sh - script: echo > ./test/test_value_pass1.txt - b: - name: "任务2" - run: - type: deno - script: let a = 1+4; a*2 -``` -在上面的描述中: -- 任务 `b` 是一个用内置 `deno` 来执行的任务,其返回值显然是 `10` -- 随后 `a` 会被执行,输入值将以字符串的形式拼接到 `script` 的最后面,即以下指令被执行: - `echo > ./test/test_value_pass1.txt 10` -- 执行结束后,会得到一个文件 `test/test_value_pass1.txt`,其中的内容就会是 `10` 。 +**explain:** -**Notice:** 当前 deno 执行只支持有返回值,但输入值并未实现(`deno_core` 的一些接口问题导致)。 +First, we define `SimpleAction` and implement the `Action` trait for this structure. In the rewritten run function, we simply get the output value of the predecessor task and multiply it by the environment variable `base`. Then accumulate the multiplied result to itself self.0. -**如何运行?** +After defining the specific task logic, start creating the prerequisites for executing `Dag`: +Initialize the global logger first. Here we set the log level to Info, and do not give the log output file, let the log output to the console by default. -在编写好 YAML 文件后,可以通过 cli 进行运行: +Create a `DefaultTask` with `SimpleAction` and give the task a name. Then set the dependencies between tasks. -```bash -$ ./target/release/dagrs --help -dagrs 0.2.0 -Command Line input +Then create a Dag and assign it a global environment variable. -USAGE: - dagrs [OPTIONS] +Finally we call the `start` function of `Dag` to execute all tasks. After the task is executed, call the `get_result` function to obtain the final execution result of the task. -ARGS: - YAML file path +The graph formed by the task is shown below: -OPTIONS: - -h, --help Print help information - -l, --logpath Log file path - -V, --version Print version information +``` + B + ↗ ↘ + A D + ↘ ↗ + C ``` -例如运行上述带输入输出的 YAML 的情况: +The execution order is a->c->b->d. ```bash -$ ./target/release/dagrs test/test_value_pass1.yaml -08:31:43 [INFO] [Start] -> 任务2 -> 任务1 -> [End] -08:31:43 [INFO] Executing Task[name: 任务2] -cargo:rerun-if-changed=/Users/wyffeiwhe/.cargo/registry/src/mirrors.ustc.edu.cn-61ef6e0cd06fb9b8/deno_core-0.121.0/00_primordials.js -cargo:rerun-if-changed=/Users/wyffeiwhe/.cargo/registry/src/mirrors.ustc.edu.cn-61ef6e0cd06fb9b8/deno_core-0.121.0/01_core.js -cargo:rerun-if-changed=/Users/wyffeiwhe/.cargo/registry/src/mirrors.ustc.edu.cn-61ef6e0cd06fb9b8/deno_core-0.121.0/02_error.js -08:31:43 [INFO] Finish Task[name: 任务2], success: true -08:31:43 [INFO] Executing Task[name: 任务1] -08:31:43 [INFO] Finish Task[name: 任务1], success: true +$cargo run +[Start] -> Task a -> Task c -> Task b -> Task d -> [End] +Executing Task[name: Task a] +Task executed successfully. [name: Task a] +Executing Task[name: Task b] +Executing Task[name: Task c] +Task executed successfully. [name: Task b] +Task executed successfully. [name: Task c] +Executing Task[name: Task d] +Task executed successfully. [name: Task d] + +Process finished with exit code 0 ``` -可以看到详细的运行情况输出,同时 log 文件可在 `$HOME/.dagrs/dagrs.log` 下找到(这是默认地址,可以通过 `-l` 选项来自定义。 +### Use the dagrs command -log 文件记录任务的执行顺序以及执行结果,其内容如下: +First use the `cargo build --release` command to compile the project, requiring rust version >=1.70. -```log -$ cat ~/.dagrs/dagrs.log -08:31:43 [INFO] [Start] -> 任务2 -> 任务1 -> [End] -08:31:43 [INFO] Executing Task[name: 任务2] -08:31:43 [INFO] Finish Task[name: 任务2], success: true -08:31:43 [INFO] Executing Task[name: 任务1] -08:31:43 [INFO] Finish Task[name: 任务1], success: true +```bash +$ .\target\release\dagrs.exe --help +Usage: dagrs.exe [OPTIONS] --yaml + +Options: + --log-path Log output file, the default is to print to the terminal + --yaml yaml configuration file path + --log-level Log level, the default is Info + -h, --help Print help + -V, --version Print version ``` +**parameter explanation:** +- The parameter yaml represents the path of the yaml configuration file and is a required parameter. +- The parameter log-path represents the path of the log output file and is an optional parameter. If not specified, the log is printed on the console by default. +- The parameter log-level represents the log output level, which is an optional parameter and defaults to info. -## TaskTrait +We can try an already defined file at `tests/config/correct.yaml` -Rust Programmer 可以通过实现 `TaskTrait` 来更灵活的定义自己的任务。 `TaskTrait` 的定义如下: - -```rust -/// Task Trait. -/// -/// Any struct implements this trait can be added into dagrs. -pub trait TaskTrait { - fn run(&self, input: Inputval, env: EnvVar) -> Retval; -} +```bash +$.\target\release\dagrs.exe --yaml=./tests/config/correct.yaml +[Start] -> Task 8 -> Task 5 -> Task 7 -> Task 6 -> Task 3 -> Task 2 -> Task 1 -> Task 4 -> [End] +Executing Task[name: Task 8] +Executing Task[name: Task 5] +Executing Task[name: Task 7] +g +Executing Task[name: Task 6] +f +Executing Task[name: Task 3] +Executing Task[name: Task 2] +Executing Task[name: Task 4] +Executing Task[name: Task 1] ``` -- `run` 是任务的执行内容,在被调度执行时由 dagrs 调用。 -- `input` 是任务的输入,由 `dagrs` 来管理。 -- `env` 是整个 `dagrs` 的全局变量,所有任务可见。 -- `Retval` 是任务的返回值。 +## Analyze the logic of task execution +**The execution process of Dag is roughly as follows:** -程序员实现的 task struct 需要放到 `TaskWrapper` 中进行使用,并通过其提供的 `exec_after` 和 `input_from` 函数进行依赖设置,具体可见下方的例子。 +- The user gives a list of tasks `tasks`. These tasks can be parsed from configuration files, or provided by user programming implementations. +- Internally generate`Graph`based on task dependencies, and generate execution sequences based on* `rely_graph`. +- The task is scheduled to start executing asynchronously. +- The task will wait to get the result`execute_states`generated by the execution of the predecessor task. +- If the result of the predecessor task can be obtained, check the continuation status`can_continue`, if it is true, continue to execute the defined logic, if it is false, trigger`handle_error`, and cancel the execution of the subsequent task. +- After all tasks are executed, set the continuation status to false, which means that the tasks of the dag cannot be scheduled for execution again. +![image-20230621223120581](assets/execute_logic.png) -**如何使用?** +## The examples -一个[例子](./examples/hello.rs)如下: +### Basic function usage -```rust -extern crate dagrs; +`examples/compute_dag.rs`: Use a custom macro to generate multiple simple tasks. -use dagrs::{DagEngine, EnvVar, Inputval, Retval, TaskTrait, TaskWrapper, init_logger}; +`examples/impl_action.rs`: Define a simple Action to build multiple tasks with the same logic. -struct T1 {} +`examples/yaml_dag.rs`: Spawn multiple tasks with a given yaml configuration file。 -impl TaskTrait for T1 { - fn run(&self, _input: Inputval, _env: EnvVar) -> Retval { - let hello_dagrs = String::from("Hello Dagrs!"); - Retval::new(hello_dagrs) - } -} +`examples/use_macro.rs`: Use the `gen_task` macro provided by dagrs to generate multiple simple tasks。 -struct T2 {} +`examples/engine.rs`: Using `Engine` to manage multiple dags with different task types. -impl TaskTrait for T2 { - fn run(&self, mut input: Inputval, _env: EnvVar) -> Retval { - let val = input.get::(0).unwrap(); - println!("{}", val); - Retval::empty() - } -} - -fn main() { - // Use dagrs provided logger - init_logger(None); +### Advanced Features - let t1 = TaskWrapper::new(T1{}, "Task 1"); - let mut t2 = TaskWrapper::new(T2{}, "Task 2"); - let mut dagrs = DagEngine::new(); +`examples/custom_task.rs`: Implement the `Task` trait and define your own task type. - // Set up dependencies - t2.exec_after(&[&t1]); - t2.input_from(&[&t1]); +`examples/custom_parser.rs`: Implement the `Parser` trait to define your own task configuration file parser。 - dagrs.add_tasks(vec![t1, t2]); - assert!(dagrs.run().unwrap()) -} +`examples/custom_log.rs`: Implement the `Logger` trait to define your own global logger. -``` +## Contribution -运行的输出如下: +Thank you for considering contributing to `dagrs`! This project enforces the [DCO](https://developercertificate.org). Contributors sign-off that they adhere to these requirements by adding a Signed-off-by line to commit messages. Git even has a -s command line option to append this automatically to your commit message: ```bash -08:45:24 [INFO] [Start] -> Task 1 -> Task 2 -> [End] -08:45:24 [INFO] Executing Task[name: Task 1] -08:45:24 [INFO] Finish Task[name: Task 1], success: true -08:45:24 [INFO] Executing Task[name: Task 2] -Hello Dagrs! -08:45:24 [INFO] Finish Task[name: Task 2], success: true -``` - -一些解释: -- `input` 提供一个 `get` 方法,用来获取任务的输入值,其接受一个输入值存放的 `index`。 - - 当定义只有一个输入值来源时(如例子中 `t2` 的输入只来自 `t1`),那么将 `index` 设置为 0 即可。 - - 如果有多个来源,假设 `t3.input_from(&[&t2, &t1])`,那么 index 就是定义任务输入时,任务的排列顺序(`&[&t2, &t1]`,如 `get(0)` 就是拿 `t2` 的返回值,`get(1)` 就是拿 `t1` 的返回值。 -- `env` 提供 `get` 和 `set`,[例子参考](./examples/hello_env.rs)。 - - `set` 即设置环境变量,其名称必须是一个字符串。 - - `get` 即获取一个环境变量的值。 -- `Retval` 是任务的返回值,提供 `new` 和 `empty` 两个方法。 - -**Notice:** 整个自定义的任务都应该是 `Sync` 和 `Send` 的,原因是:任务是被放到一个线程中执行调度的。 - - -**如何运行脚本?** - -程序员可以通过 `RunScript` 结构来实现脚本的运行(当然也可以直接在代码里自行运行而不通过该结构体),定义如下: - -```rust -pub struct RunScript { - script: String, - executor: RunType, -} - -``` - -这里: -- `script` 即脚本,可以是命令本身("echo hello!"),也可以是脚本的路径("./test/test.sh")。 -- `executor` 是任务的执行方式,`RunType` 是一个 enum 类型: - ```rust - pub enum RunType { - SH, - DENO, - } - ``` - -`RunScript` 提供了 `exec` 函数: -```rust -pub fn exec(&self, input: Inputval) -> Result {} +$ git commit -s -m 'This is my commit message' +$ git status +This is my commit message +Signed-off-by: Random J Developer ``` -如果执行正常,则将结果以 `String` 的形式返回,否则返回一个 `DagError` 的错误类型。 - -一个简单的[例子](./examples/hello_script.rs): -```rust -extern crate dagrs; - -use dagrs::{DagEngine, EnvVar, Inputval, Retval, TaskTrait, TaskWrapper, init_logger, RunScript, RunType}; - -struct T {} - -impl TaskTrait for T { - fn run(&self, _input: Inputval, _env: EnvVar) -> Retval { - let script = RunScript::new("echo 'Hello Dagrs!'", RunType::SH); - - let res = script.exec(None); - println!("{:?}", res); - Retval::empty() - } -} -fn main() { - // Use dagrs provided logger - init_logger(None); +## License - let t = TaskWrapper::new(T{}, "Task"); - let mut dagrs = DagEngine::new(); - - dagrs.add_tasks(vec![t]); - assert!(dagrs.run().unwrap()) -} -``` - -其执行结果为: -```bash -09:12:48 [INFO] [Start] -> Task -> [End] -09:12:48 [INFO] Executing Task[name: Task] -Ok("Hello Dagrs!\n") -09:12:48 [INFO] Finish Task[name: Task], success: true -``` +Freighter is licensed under this Licensed: +* MIT LICENSE ([LICENSE-MIT](LICENSE-MIT) or https://opensource.org/licenses/MIT) +* Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or https://www.apache.org/licenses/LICENSE-2.0) diff --git a/dagrs/assets/execute_logic.png b/dagrs/assets/execute_logic.png new file mode 100644 index 0000000000000000000000000000000000000000..9b33e5ae9eea2d4af6256a71089c191c4f6621d0 GIT binary patch literal 56356 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 literal 0 HcmV?d00001 diff --git a/dagrs/examples/compute_dag.rs b/dagrs/examples/compute_dag.rs new file mode 100644 index 00000000..cf0a6454 --- /dev/null +++ b/dagrs/examples/compute_dag.rs @@ -0,0 +1,67 @@ +//! Only use Dag, execute a job. The graph is as follows: +//! +//! ↱----------↴ +//! B -→ E --→ G +//! ↗ ↗ ↗ +//! A --→ C / +//! ↘ ↘ / +//! D -→ F +//! +//! The final execution result is 272. + +extern crate dagrs; + +use std::sync::Arc; + +use dagrs::{ + Action, + Dag, DefaultTask, EnvVar,log, Input, Output, RunningError,LogLevel +}; + +macro_rules! generate_task { + ($action:ident($val:expr),$name:expr) => {{ + pub struct $action(usize); + impl Action for $action { + fn run(&self, input: Input, env: Arc) -> Result { + let base = env.get::("base").unwrap(); + let mut sum = self.0; + input + .get_iter() + .for_each(|i| sum += i.get::().unwrap() * base); + Ok(Output::new(sum)) + } + } + DefaultTask::new($action($val), $name) + }}; +} + +fn main() { + // initialization log. + log::init_logger(LogLevel::Info, None); + // generate some tasks. + let a = generate_task!(A(1), "Compute A"); + let mut b = generate_task!(B(2), "Compute B"); + let mut c = generate_task!(C(4), "Compute C"); + let mut d = generate_task!(D(8), "Compute D"); + let mut e = generate_task!(E(16), "Compute E"); + let mut f = generate_task!(F(32), "Compute F"); + let mut g = generate_task!(G(64), "Compute G"); + // Set up task dependencies. + b.set_predecessors(&[&a]); + c.set_predecessors(&[&a]); + d.set_predecessors(&[&a]); + e.set_predecessors(&[&b, &c]); + f.set_predecessors(&[&c, &d]); + g.set_predecessors(&[&b, &e, &f]); + // Create a new Dag. + let mut dag = Dag::with_tasks(vec![a, b, c, d, e, f, g]); + // Set a global environment variable for this dag. + let mut env = EnvVar::new(); + env.set("base", 2usize); + dag.set_env(env); + // Start executing this dag + assert!(dag.start().unwrap()); + // Get execution result. + let res = dag.get_result::().unwrap(); + println!("The result is {}.", res); +} diff --git a/dagrs/examples/custom_log.rs b/dagrs/examples/custom_log.rs new file mode 100644 index 00000000..522de268 --- /dev/null +++ b/dagrs/examples/custom_log.rs @@ -0,0 +1,63 @@ +//! Use the simplelog and log libraries to implement the Logger trait to customize the log manager. + +extern crate dagrs; +extern crate log; +extern crate simplelog; + +use dagrs::{Dag, LogLevel, Logger}; +use simplelog::*; + +struct MyLogger { + level: LogLevel, +} + +impl MyLogger { + /// Create MyLogger and set the log level of simplelog. + fn new(level: LogLevel) -> Self { + let filter = match level { + LogLevel::Debug => LevelFilter::Debug, + LogLevel::Info => LevelFilter::Info, + LogLevel::Warn => LevelFilter::Warn, + LogLevel::Error => LevelFilter::Error, + LogLevel::Off => LevelFilter::Off, + }; + CombinedLogger::init(vec![TermLogger::new( + filter, + Config::default(), + TerminalMode::Mixed, + ColorChoice::Auto, + )]) + .unwrap(); + MyLogger { level } + } +} + +/// In turn, use the corresponding logging macro of log to override the Logger method. +impl Logger for MyLogger { + fn level(&self) -> LogLevel { + self.level + } + + fn debug(&self, msg: String) { + log::debug!("{}", msg); + } + + fn info(&self, msg: String) { + log::info!("{}", msg); + } + + fn warn(&self, msg: String) { + log::warn!("{}", msg); + } + + fn error(&self, msg: String) { + log::error!("{}", msg); + } +} + +fn main() { + // Initialize the global logger with a custom logger. + dagrs::log::init_custom_logger(MyLogger::new(LogLevel::Info)); + let mut dag = Dag::with_yaml("tests/config/correct.yaml").unwrap(); + assert!(dag.start().unwrap()); +} diff --git a/dagrs/examples/custom_parser.rs b/dagrs/examples/custom_parser.rs new file mode 100644 index 00000000..97514e46 --- /dev/null +++ b/dagrs/examples/custom_parser.rs @@ -0,0 +1,155 @@ +//! Implement the Parser interface to customize the task configuration file parser. +//! The content of the configuration file is as follows: +//! +//! ``` +//! a,Task a,b c,sh,echo a +//! b,Task b,c f g,sh,echo b +//! c,Task c,e g,sh,echo c +//! d,Task d,c e,sh,echo d +//! e,Task e,h,sh,echo e +//! f,Task f,g,deno,Deno.core.print("f\n") +//! g,Task g,h,deno,Deno.core.print("g\n") +//! h,Task h,,sh,echo h +//! ``` + +extern crate dagrs; + +use std::{fs, sync::Arc}; +use std::collections::HashMap; +use std::fmt::{Display, Formatter}; + +use dagrs::{Action, Dag, log,LogLevel, JavaScript, Parser, ParserError, ShScript, Task}; + +struct MyTask { + tid: (String, usize), + name: String, + precursors: Vec, + precursors_id: Vec, + action: Arc, +} + +impl MyTask { + pub fn new( + txt_id: &str, + precursors: Vec, + name: String, + action: impl Action + Send + Sync + 'static, + ) -> Self { + Self { + tid: (txt_id.to_owned(), dagrs::alloc_id()), + name, + precursors, + precursors_id: Vec::new(), + action: Arc::new(action), + } + } + + pub fn init_precursors(&mut self, pres_id: Vec) { + self.precursors_id = pres_id; + } + + pub fn str_precursors(&self) -> Vec { + self.precursors.clone() + } + + pub fn str_id(&self) -> String { + self.tid.0.clone() + } +} + +impl Task for MyTask { + fn action(&self) -> Arc { + self.action.clone() + } + fn predecessors(&self) -> &[usize] { + &self.precursors_id + } + fn id(&self) -> usize { + self.tid.1 + } + fn name(&self) -> String { + self.name.clone() + } +} + +impl Display for MyTask { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{},{},{},{:?}", self.name, self.tid.0, self.tid.1, self.precursors) + } +} + +struct ConfigParser; + +impl ConfigParser { + fn load_file(&self, file: &str) -> Result, ParserError> { + let contents = fs::read_to_string(file)?; + let lines: Vec = contents.lines().map(|line| line.to_string()).collect(); + Ok(lines) + } + + fn parse_one(&self, item: String) -> MyTask { + let attr: Vec<&str> = item.split(",").collect(); + + let pres_item = attr.get(2).unwrap().clone(); + let pres = if pres_item.eq("") { + Vec::new() + } else { + pres_item.split(" ").map(|pre| pre.to_string()).collect() + }; + + let id = attr.get(0).unwrap().clone(); + let name = attr.get(1).unwrap().to_string(); + let script = attr.get(4).unwrap().clone(); + let t_type = attr.get(3).unwrap().clone(); + if t_type.eq("sh") { + MyTask::new( + id, + pres, + name, + ShScript::new(script), + ) + } else { + MyTask::new( + id, + pres, + name, + JavaScript::new(script), + ) + } + } +} + +impl Parser for ConfigParser { + fn parse_tasks(&self, file: &str) -> Result>, ParserError> { + let content = self.load_file(file)?; + let mut map = HashMap::new(); + let mut tasks = Vec::new(); + content.into_iter().for_each(|line| { + let task = self.parse_one(line); + map.insert(task.str_id(), task.id()); + tasks.push(task); + }); + + for task in tasks.iter_mut() { + let mut pres = Vec::new(); + let str_pre = task.str_precursors(); + if !str_pre.is_empty() { + for pre in str_pre { + pres.push(map[&pre[..]]); + } + task.init_precursors(pres); + } + } + Ok(tasks + .into_iter() + .map(|task| Box::new(task) as Box) + .collect()) + } +} + +fn main() { + log::init_logger(LogLevel::Info, None); + let file = "tests/config/custom_file_task.txt"; + let mut dag = Dag::with_config_file_and_parser(file, Box::new(ConfigParser)).unwrap(); + assert!(dag.start().unwrap()); +} \ No newline at end of file diff --git a/dagrs/examples/custom_task.rs b/dagrs/examples/custom_task.rs new file mode 100644 index 00000000..7d1a224b --- /dev/null +++ b/dagrs/examples/custom_task.rs @@ -0,0 +1,92 @@ +//! Implement the Task trait to customize task properties. +//! MyTask is basically the same as DefaultTask provided by dagrs. + +use std::sync::Arc; + +use dagrs::{log, Action, Dag, EnvVar, Input, LogLevel, Output, RunningError, Task,alloc_id}; + +struct MyTask { + id: usize, + name: String, + predecessor_tasks: Vec, + action: Arc, +} + +impl MyTask { + pub fn new(action: impl Action + 'static + Send + Sync, name: &str) -> Self { + MyTask { + id: alloc_id(), + action: Arc::new(action), + name: name.to_owned(), + predecessor_tasks: Vec::new(), + } + } + + pub fn set_predecessors(&mut self, predecessors: &[&MyTask]) { + self.predecessor_tasks + .extend(predecessors.iter().map(|t| t.id())) + } +} + +impl Task for MyTask { + fn action(&self) -> Arc { + self.action.clone() + } + + fn predecessors(&self) -> &[usize] { + &self.predecessor_tasks + } + + fn id(&self) -> usize { + self.id + } + + fn name(&self) -> String { + self.name.clone() + } +} + +macro_rules! generate_task { + ($action:ident($val:expr),$name:expr) => {{ + pub struct $action(usize); + impl Action for $action { + fn run(&self, input: Input, env: Arc) -> Result { + let base = env.get::("base").unwrap(); + let mut sum = self.0; + input + .get_iter() + .for_each(|i| sum += i.get::().unwrap() * base); + Ok(Output::new(sum)) + } + } + MyTask::new($action($val), $name) + }}; +} + +fn main() { + log::init_logger(LogLevel::Info, None); + let a = generate_task!(A(1), "Compute A"); + let mut b = generate_task!(B(2), "Compute B"); + let mut c = generate_task!(C(4), "Compute C"); + let mut d = generate_task!(D(8), "Compute D"); + let mut e = generate_task!(E(16), "Compute E"); + let mut f = generate_task!(F(32), "Compute F"); + let mut g = generate_task!(G(64), "Compute G"); + + b.set_predecessors(&[&a]); + c.set_predecessors(&[&a]); + d.set_predecessors(&[&a]); + e.set_predecessors(&[&b, &c]); + f.set_predecessors(&[&c, &d]); + g.set_predecessors(&[&b, &e, &f]); + + let mut env = EnvVar::new(); + env.set("base", 2usize); + + let mut dag = Dag::with_tasks(vec![a, b, c, d, e, f, g]); + dag.set_env(env); + assert!(dag.start().unwrap()); + + let res = dag.get_result::().unwrap(); + println!("The result is {}.", res); +} diff --git a/dagrs/examples/engine.rs b/dagrs/examples/engine.rs new file mode 100644 index 00000000..9dd2b80c --- /dev/null +++ b/dagrs/examples/engine.rs @@ -0,0 +1,89 @@ +//! Use Engine to manage multiple Dag jobs. + +extern crate dagrs; + +use std::sync::Arc; + +use dagrs::{ + gen_task, log, Action, Dag, DefaultTask, Engine, EnvVar, Input, LogLevel, Output, RunningError, +}; +fn main() { + // initialization log. + log::init_logger(LogLevel::Error, None); + // Create an Engine. + let mut engine = Engine::default(); + + // Create some task for dag1. + let t1_a = gen_task!("Compute A1", |_input: Input, _env: Arc| Ok( + Output::new(20usize) + )); + let mut t1_b = gen_task!("Compute B1", |input: Input, _env: Arc| { + let mut sum = 10; + input.get_iter().for_each(|input| { + sum += input.get::().unwrap(); + }); + Ok(Output::new(sum)) + }); + let mut t1_c = gen_task!("Compute C1", |input: Input, _env: Arc| { + let mut sum = 20; + input.get_iter().for_each(|input| { + sum += input.get::().unwrap(); + }); + Ok(Output::new(sum)) + }); + + let mut t1_d = gen_task!("Compute D1", |input: Input, _env: Arc| { + let mut sum = 30; + input.get_iter().for_each(|input| { + sum += input.get::().unwrap(); + }); + Ok(Output::new(sum)) + }); + t1_b.set_predecessors(&[&t1_a]); + t1_c.set_predecessors(&[&t1_a]); + t1_d.set_predecessors(&[&t1_b, &t1_c]); + let dag1 = Dag::with_tasks(vec![t1_a, t1_b, t1_c, t1_d]); + // Add dag1 to engine. + engine.append_dag("graph1", dag1); + + // Create some task for dag2. + let t2_a = gen_task!("Compute A2", |_input: Input, _env: Arc| Ok( + Output::new(2usize) + )); + let mut t2_b = gen_task!("Compute B2", |input: Input, _env: Arc| { + let mut sum=4; + input.get_iter().for_each(|input|{ + sum *= input.get::().unwrap(); + }); + Ok(Output::new(sum)) + }); + let mut t2_c = gen_task!("Compute C2", |input: Input, _env: Arc| { + let mut sum=8; + input.get_iter().for_each(|input|{ + sum *= input.get::().unwrap(); + }); + Ok(Output::new(sum)) + }); + let mut t2_d = gen_task!("Compute D2", |input: Input, _env: Arc| { + let mut sum=16; + input.get_iter().for_each(|input|{ + sum *= input.get::().unwrap(); + }); + Ok(Output::new(sum)) + }); + t2_b.set_predecessors(&[&t2_a]); + t2_c.set_predecessors(&[&t2_b]); + t2_d.set_predecessors(&[&t2_c]); + let dag2 = Dag::with_tasks(vec![t2_a, t2_b, t2_c, t2_d]); + // Add dag2 to engine. + engine.append_dag("graph2", dag2); + // Read tasks from configuration files and resolve to dag3. + let dag3 = Dag::with_yaml("tests/config/correct.yaml").unwrap(); + // Add dag3 to engine. + engine.append_dag("graph3", dag3); + // Execute dag in order, the order should be dag1, dag2, dag3. + assert_eq!(engine.run_sequential(),vec![true,true,true]); + // Get the execution results of dag1 and dag2. + assert_eq!(engine.get_dag_result::("graph1").unwrap(),100); + assert_eq!(engine.get_dag_result::("graph2").unwrap(),1024); +} diff --git a/dagrs/examples/hello.rs b/dagrs/examples/hello.rs deleted file mode 100644 index f25fd4e5..00000000 --- a/dagrs/examples/hello.rs +++ /dev/null @@ -1,38 +0,0 @@ -extern crate dagrs; - -use dagrs::{DagEngine, EnvVar, Inputval, Retval, TaskTrait, TaskWrapper, init_logger}; - -struct T1 {} - -impl TaskTrait for T1 { - fn run(&self, _input: Inputval, _env: EnvVar) -> Retval { - let hello_dagrs = String::from("Hello Dagrs!"); - Retval::new(hello_dagrs) - } -} - -struct T2 {} - -impl TaskTrait for T2 { - fn run(&self, mut input: Inputval, _env: EnvVar) -> Retval { - let val = input.get::(0).unwrap(); - println!("{}", val); - Retval::empty() - } -} - -fn main() { - // Use dagrs provided logger - init_logger(None); - - let t1 = TaskWrapper::new(T1{}, "Task 1"); - let mut t2 = TaskWrapper::new(T2{}, "Task 2"); - let mut dagrs = DagEngine::new(); - - // Set up dependencies - t2.exec_after(&[&t1]); - t2.input_from(&[&t1]); - - dagrs.add_tasks(vec![t1, t2]); - assert!(dagrs.run().unwrap()) -} diff --git a/dagrs/examples/hello_env.rs b/dagrs/examples/hello_env.rs deleted file mode 100644 index 94470f5f..00000000 --- a/dagrs/examples/hello_env.rs +++ /dev/null @@ -1,39 +0,0 @@ -extern crate dagrs; - -use dagrs::{DagEngine, EnvVar, Inputval, Retval, TaskTrait, TaskWrapper, init_logger}; - -struct T1 {} - -impl TaskTrait for T1 { - fn run(&self, _input: Inputval, mut env: EnvVar) -> Retval { - let hello_dagrs = String::from("Hello Dagrs!"); - env.set("you_need_it", hello_dagrs); - Retval::empty() - } -} - -struct T2 {} - -impl TaskTrait for T2 { - fn run(&self, _input: Inputval, env: EnvVar) -> Retval { - let val = env.get::("you_need_it").unwrap(); - println!("{}", val); - Retval::empty() - } -} - -fn main() { - // Use dagrs provided logger - init_logger(None); - - let t1 = TaskWrapper::new(T1{}, "Task 1"); - let mut t2 = TaskWrapper::new(T2{}, "Task 2"); - let mut dagrs = DagEngine::new(); - - // Set up dependencies - t2.exec_after(&[&t1]); - t2.input_from(&[&t1]); - - dagrs.add_tasks(vec![t1, t2]); - assert!(dagrs.run().unwrap()) -} diff --git a/dagrs/examples/hello_script.rs b/dagrs/examples/hello_script.rs deleted file mode 100644 index 191f887e..00000000 --- a/dagrs/examples/hello_script.rs +++ /dev/null @@ -1,26 +0,0 @@ -extern crate dagrs; - -use dagrs::{DagEngine, EnvVar, Inputval, Retval, TaskTrait, TaskWrapper, init_logger, RunScript, RunType}; - -struct T {} - -impl TaskTrait for T { - fn run(&self, _input: Inputval, _env: EnvVar) -> Retval { - let script = RunScript::new("echo 'Hello Dagrs!'", RunType::SH); - - let res = script.exec(None); - println!("{:?}", res); - Retval::empty() - } -} - -fn main() { - // Use dagrs provided logger - init_logger(None); - - let t = TaskWrapper::new(T{}, "Task"); - let mut dagrs = DagEngine::new(); - - dagrs.add_tasks(vec![t]); - assert!(dagrs.run().unwrap()) -} diff --git a/dagrs/examples/impl_action.rs b/dagrs/examples/impl_action.rs new file mode 100644 index 00000000..bc5b43a3 --- /dev/null +++ b/dagrs/examples/impl_action.rs @@ -0,0 +1,49 @@ +//! Implement the Action trait to define the task logic. + +extern crate dagrs; + +use std::sync::Arc; + +use dagrs::{ + Action, + Dag, DefaultTask, EnvVar, Input, log, LogLevel, Output, RunningError, +}; + +struct SimpleAction(usize); + +/// Implement the `Action` trait for `SimpleAction`, defining the logic of the `run` function. +/// The logic here is simply to get the output value (usize) of all predecessor tasks and then accumulate. +impl Action for SimpleAction { + fn run(&self, input: Input, env: Arc) -> Result { + let base = env.get::("base").unwrap(); + let mut sum = self.0; + input + .get_iter() + .for_each(|i| sum += i.get::().unwrap() * base); + Ok(Output::new(sum)) + } +} + +fn main() { + // Initialize the global logger + log::init_logger(LogLevel::Info, None); + // Generate four tasks. + let a = DefaultTask::new(SimpleAction(10), "Task a"); + let mut b = DefaultTask::new(SimpleAction(20), "Task b"); + let mut c = DefaultTask::new(SimpleAction(30), "Task c"); + let mut d = DefaultTask::new(SimpleAction(40), "Task d"); + // Set the precursor for each task. + b.set_predecessors(&[&a]); + c.set_predecessors(&[&a]); + d.set_predecessors(&[&b, &c]); + // Take these four tasks as a Dag. + let mut dag = Dag::with_tasks(vec![a, b, c, d]); + // Set a global environment variable for this dag. + let mut env = EnvVar::new(); + env.set("base", 2usize); + dag.set_env(env); + // Begin execution. + assert!(dag.start().unwrap()); + // Get execution result + assert_eq!(dag.get_result::().unwrap(), 220); +} \ No newline at end of file diff --git a/dagrs/examples/use_macro.rs b/dagrs/examples/use_macro.rs new file mode 100644 index 00000000..1d3af746 --- /dev/null +++ b/dagrs/examples/use_macro.rs @@ -0,0 +1,56 @@ +//! Use the gen_task provided by dagrs! Macros define simple tasks. +//! Execute graph: +//! B +//! ↗ ↘ +//! A D +//! ↘ ↗ +//! C + +extern crate dagrs; + +use std::sync::Arc; + +use dagrs::{ + gen_task, log, Action, Dag, DefaultTask, EnvVar, Input, LogLevel, Output, RunningError, +}; + +fn main() { + log::init_logger(LogLevel::Info, None); + let a = gen_task!("Compute A", |_input: Input, _env: Arc| Ok( + Output::new(20usize) + )); + let mut b = gen_task!("Compute B", |input: Input, _env: Arc| { + let mut sum = 0; + input + .get_iter() + .for_each(|i| sum += i.get::().unwrap()); + Ok(Output::new(sum)) + }); + + let mut c = gen_task!("Compute C", |input: Input, env: Arc| { + let mut sum = 0; + let base = env.get::("base").unwrap(); + input + .get_iter() + .for_each(|i| sum += i.get::().unwrap() * base); + Ok(Output::new(sum)) + }); + let mut d = gen_task!("Compute D", |input: Input, env: Arc| { + let mut sum = 0; + let base = env.get::("base").unwrap(); + input + .get_iter() + .for_each(|i| sum += i.get::().unwrap() - base); + Ok(Output::new(sum)) + }); + + b.set_predecessors(&[&a]); + c.set_predecessors(&[&a]); + d.set_predecessors(&[&b, &c]); + let mut job = Dag::with_tasks(vec![a, b, c, d]); + let mut env = EnvVar::new(); + env.set("base", 2usize); + job.set_env(env); + assert!(job.start().unwrap()); + assert_eq!(job.get_result::().unwrap(), 56); +} diff --git a/dagrs/examples/yaml_dag.rs b/dagrs/examples/yaml_dag.rs new file mode 100644 index 00000000..044c6e7c --- /dev/null +++ b/dagrs/examples/yaml_dag.rs @@ -0,0 +1,11 @@ +//! Read the task information configured in the yaml file. + +extern crate dagrs; + +use dagrs::{log, Dag, LogLevel}; + +fn main() { + log::init_logger(LogLevel::Info, None); + let mut job = Dag::with_yaml("tests/config/correct.yaml").unwrap(); + assert!(job.start().unwrap()); +} diff --git a/dagrs/src/engine/dag.rs b/dagrs/src/engine/dag.rs new file mode 100644 index 00000000..fc39591c --- /dev/null +++ b/dagrs/src/engine/dag.rs @@ -0,0 +1,337 @@ +//! The Dag +//! +//! # [`Dag`] is dagrs's main body. +//! +//! [`Dag`] embodies the scheduling logic of tasks written by users or tasks in a given configuration file. +//! A Dag contains multiple tasks. This task can be added to a Dag as long as it implements +//! the [`Task`] trait, and the user needs to define specific execution logic for the task, that is, +//! implement the [`Action`] trait and override the `run` method. +//! +//! The execution process of Dag is roughly as follows: +//! - The user gives a list of tasks `tasks`. These tasks can be parsed from configuration files, or provided +//! by user programming implementations. +//! - Internally generate [`Graph`] based on task dependencies, and generate execution sequences based on `rely_graph`. +//! - The task is scheduled to start executing asynchronously. +//! - The task will wait to get the result `execute_states` generated by the execution of the predecessor task. +//! - If the result of the predecessor task can be obtained, check the continuation status `can_continue`, if it +//! is true, continue to execute the defined logic, if it is false, trigger `handle_error`, and cancel the +//! execution of the subsequent task. +//! - After all tasks are executed, set the continuation status to false, which means that the tasks of the dag +//! cannot be scheduled for execution again. +//! +//! # Example +//! ```rust +//! use dagrs::{log,LogLevel,Dag, DefaultTask, gen_task, Output,Input,EnvVar,RunningError,Action}; +//! use std::sync::Arc; +//! log::init_logger(LogLevel::Info,None); +//! let task=gen_task!("Simple Task",|input,_env|{ +//! Ok(Output::new(1)) +//! }); +//! let mut dag=Dag::with_tasks(vec![task]); +//! assert!(dag.start().unwrap()) +//! +//! ``` + +use std::{ + collections::HashMap, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, +}; + +use anymap2::any::CloneAnySendSync; +use tokio::task::JoinHandle; + +use crate::{ + parser::{Parser, YamlParser}, + task::{ExecState, Input, Task}, + utils::{log, EnvVar}, +}; + +use super::{error::DagError, graph::Graph}; + +/// A Dag represents a set of tasks. Use it to build a multitasking Dag. +#[derive(Debug)] +pub struct Dag { + /// Store all tasks' infos. + /// + /// Arc but no mutex, because only one thread will change [`TaskWrapper`]at a time. + /// And no modification to [`TaskWrapper`] happens during the execution of it. + tasks: HashMap>>, + /// Store dependency relations. + rely_graph: Graph, + /// Store a task's running result.Execution results will be read and written asynchronously by several threads. + execute_states: HashMap>, + /// Global environment variables for this Dag job. It should be set before the Dag job runs. + env: Arc, + /// Mark whether the Dag task can continue to execute. + /// When an error occurs during the execution of any task, this flag will be set to false, and + /// subsequent tasks will be canceled. + /// when all tasks in the dag are executed, the flag will also be set to false, indicating that + /// the task cannot be run repeatedly. + can_continue: Arc, + /// The execution sequence of tasks. + exe_sequence: Vec, +} + +impl Dag { + /// Create a dag. This function is not open to the public. There are three ways to create a new + /// dag, corresponding to three functions: `with_tasks`, `with_yaml`, `with_config_file_and_parser`. + fn new() -> Dag { + Dag { + tasks: HashMap::new(), + rely_graph: Graph::new(), + execute_states: HashMap::new(), + env: Arc::new(EnvVar::new()), + can_continue: Arc::new(AtomicBool::new(true)), + exe_sequence: Vec::new(), + } + } + + /// Create a dag by adding a series of tasks. + pub fn with_tasks(tasks: Vec) -> Dag { + let mut dag = Dag::new(); + tasks.into_iter().for_each(|task| { + let task = Box::new(task) as Box; + dag.tasks.insert(task.id(), Arc::new(task)); + }); + dag + } + + /// Given a yaml configuration file parsing task to generate a dag. + pub fn with_yaml(file: &str) -> Result { + Dag::read_tasks(file, None) + } + + /// Generates a dag with the user given path to a custom parser and task config file. + pub fn with_config_file_and_parser( + file: &str, + parser: Box, + ) -> Result { + Dag::read_tasks(file, Some(parser)) + } + + /// Parse the content of the configuration file into a series of tasks and generate a dag. + fn read_tasks(file: &str, parser: Option>) -> Result { + let mut dag = Dag::new(); + let tasks = match parser { + Some(p) => p.parse_tasks(file)?, + None => { + let parser = YamlParser; + parser.parse_tasks(file)? + } + }; + tasks.into_iter().for_each(|task| { + dag.tasks.insert(task.id(), Arc::new(task)); + }); + Ok(dag) + } + + /// create rely map between tasks. + /// + /// This operation will initialize `dagrs.rely_graph` if no error occurs. + fn create_graph(&mut self) -> Result<(), DagError> { + let size = self.tasks.len(); + self.rely_graph.set_graph_size(size); + + // Add Node (create id - index mapping) + self.tasks + .iter() + .map(|(&n, _)| self.rely_graph.add_node(n)) + .count(); + + // Form Graph + for (&id, task) in self.tasks.iter() { + let index = self.rely_graph.find_index_by_id(&id).unwrap(); + + for rely_task_id in task.predecessors() { + // Rely task existence check + let rely_index = self + .rely_graph + .find_index_by_id(rely_task_id) + .ok_or(DagError::RelyTaskIllegal(task.name()))?; + + self.rely_graph.add_edge(rely_index, index); + } + } + + Ok(()) + } + + /// Initialize dags. The initialization process completes three actions: + /// - Initialize the status of each task execution result. + /// - Create a graph from task dependencies. + /// - Generate task heart sequence according to topological sorting of graph. + pub(crate) fn init(&mut self) -> Result<(), DagError> { + self.tasks.keys().for_each(|id| { + self.execute_states + .insert(*id, Arc::new(ExecState::new(*id))); + }); + + self.create_graph()?; + + match self.rely_graph.topo_sort() { + Some(seq) => { + if seq.is_empty() { + return Err(DagError::EmptyJob); + } + let exe_seq: Vec = seq + .into_iter() + .map(|index| self.rely_graph.find_id_by_index(index).unwrap()) + .collect(); + self.exe_sequence = exe_seq; + Ok(()) + } + None => Err(DagError::LoopGraph), + } + } + + /// This function is used for the execution of a single dag. + pub fn start(&mut self) -> Result { + // If the current continuable state is false, the task will start failing. + if self.can_continue.load(Ordering::Acquire) { + self.init().map_or_else(Err, |_| { + Ok(tokio::runtime::Runtime::new() + .unwrap() + .block_on(async { self.run().await })) + }) + } else { + Ok(false) + } + } + + /// Execute tasks sequentially according to the execution sequence given by + /// topological sorting, and cancel the execution of subsequent tasks if an + /// error is encountered during task execution. + pub(crate) async fn run(&self) -> bool { + let mut exe_seq = String::from("[Start]"); + self.exe_sequence + .iter() + .for_each(|id| exe_seq.push_str(&format!(" -> {}", self.tasks[id].name()))); + log::info(format!("{} -> [End]", exe_seq)); + let mut handles = Vec::new(); + self.exe_sequence.iter().for_each(|id| { + handles.push((*id, self.execute_task(self.tasks[id].clone()))); + }); + // Wait for the status of each task to execute. If there is an error in the execution of a task, + // the engine will fail to execute and give up executing tasks that have not yet been executed. + let mut exe_success = true; + for handle in handles { + let complete = handle.1.await.map_or_else( + |err| { + log::error(format!( + "Task execution encountered an unexpected error! {}", + err + )); + false + }, + |state| state, + ); + if !complete { + log::error(format!( + "Task execution failed! [{}]", + self.tasks[&handle.0].name() + )); + self.handle_error(&handle.0).await; + exe_success = false; + } + } + self.can_continue.store(false, Ordering::Release); + exe_success + } + + /// Execute a given task asynchronously. + fn execute_task(&self, task: Arc>) -> JoinHandle { + let env = self.env.clone(); + let task_id = task.id(); + let task_name = task.name(); + let execute_state = self.execute_states[&task_id].clone(); + let task_out_degree = self.rely_graph.get_node_out_degree(&task_id); + let wait_for_input: Vec> = task + .predecessors() + .iter() + .map(|id| self.execute_states[id].clone()) + .collect(); + let action = task.action(); + let can_continue = self.can_continue.clone(); + tokio::spawn(async move { + // Wait for the execution result of the predecessor task + let mut inputs = Vec::new(); + for wait_for in wait_for_input { + wait_for.semaphore().acquire().await.unwrap().forget(); + // When the task execution result of the predecessor can be obtained, judge whether + // the continuation flag is set to false, if it is set to false, cancel the specific + // execution logic of the task and return immediately. + if !can_continue.load(Ordering::Acquire) { + return true; + } + if let Some(content) = wait_for.get_output() { + if !content.is_empty() { + inputs.push(content); + } + } + } + log::info(format!("Executing Task[name: {}]", task_name)); + // Concrete logical behavior for performing tasks. + match action.run(Input::new(inputs), env) { + Ok(out) => { + // Store execution results + execute_state.set_output(out); + execute_state.semaphore().add_permits(task_out_degree); + log::info(format!("Task executed successfully. [name: {}]",task_name)); + true + } + Err(err) => { + log::error(format!("Task failed[name: {}]. {}", task_name, err)); + false + } + } + }) + } + + /// error handling. + /// When a task execution error occurs, the error handling logic is: + /// First, set the continuation status to false, and then release the semaphore of the + /// error task and the tasks after the error task, so that subsequent tasks can quickly + /// know that some tasks have errors and cannot continue to execute. + /// After that, the follow-up task finds that the flag that can continue to execute is set + /// to false, and the specific behavior of executing the task will be cancelled. + async fn handle_error(&self, error_task_id: &usize) { + self.can_continue.store(false, Ordering::Release); + // Find the position of the faulty task in the execution sequence. + let index = self + .exe_sequence + .iter() + .position(|tid| *tid == *error_task_id) + .unwrap(); + + for i in index..self.exe_sequence.len() { + let tid = self.exe_sequence.get(i).unwrap(); + let out_degree = self.rely_graph.get_node_out_degree(tid); + self.execute_states + .get(tid) + .unwrap() + .semaphore() + .add_permits(out_degree); + } + } + + /// Get the final execution result. + pub fn get_result(&self) -> Option { + if self.exe_sequence.is_empty() { + None + } else { + let last_id = self.exe_sequence.last().unwrap(); + match self.execute_states[last_id].get_output() { + Some(ref content) => content.clone().remove(), + None => None, + } + } + } + + /// Before the dag starts executing, set the dag's global environment variable. + pub fn set_env(&mut self, env: EnvVar) { + self.env = Arc::new(env); + } +} diff --git a/dagrs/src/engine/dag_engine.rs b/dagrs/src/engine/dag_engine.rs deleted file mode 100644 index bfeb2b70..00000000 --- a/dagrs/src/engine/dag_engine.rs +++ /dev/null @@ -1,211 +0,0 @@ -//! Dag Engine is dagrs's main body - -use super::{ - env_variables::EnvVar, - error_handler::{DagError, RunningError}, - graph::Graph, -}; -use crate::task::{ExecState, Inputval, Retval, TaskWrapper, YamlTask}; -use log::*; -use std::{collections::HashMap, sync::Arc}; - -/// dagrs's function is wrapped in DagEngine struct -pub struct DagEngine { - /// Store all tasks' infos. - /// - /// Arc but no mutex, because only one thread will change [`TaskWrapper`] - /// at a time. And no modification to [`TaskWrapper`] happens during the execution of it. - tasks: HashMap>, - /// Store dependency relations - rely_graph: Graph, - /// Store a task's running result - execstate_store: HashMap, - // Environment Variables - env: EnvVar, -} - -impl DagEngine { - /// Allocate a new DagEngine. - /// - /// # Example - /// ``` - /// let dagrs = DagEngine::new(); - /// ``` - pub fn new() -> DagEngine { - DagEngine { - tasks: HashMap::new(), - rely_graph: Graph::new(), - execstate_store: HashMap::new(), - env: EnvVar::new(), - } - } - - /// Do dagrs's job. - /// - /// # Example - /// ``` - /// let dagrs = DagEngine::new(); - /// dagrs.add_tasks(vec![task1, task2]); - /// ``` - /// - /// Here `task1` and `task2` are user defined task wrapped in [`TaskWrapper`]. - /// - /// **Note:** This method must be called after all tasks have been added into dagrs. - pub fn run(&mut self) -> Result { - self.create_graph()?; - let rt = tokio::runtime::Runtime::new().unwrap(); - Ok(rt.block_on(async { self.check_dag().await })) - } - - /// Do dagrs's job from yaml file. - /// - /// # Example - /// ``` - /// let dagrs = DagEngine::new(); - /// dagrs.run_from_yaml("test/test_dag1.yaml"); - /// ``` - /// - /// This method is similar to `run`, but read tasks from yaml file, - /// thus no need to add tasks mannually. - pub fn run_from_yaml(mut self, filename: &str) -> Result { - self.read_tasks(filename)?; - self.run() - } - - /// Read tasks into engine through yaml. - /// - /// This operation will read all info in yaml file into `dagrs.tasks` if no error occurs. - fn read_tasks(&mut self, filename: &str) -> Result<(), DagError> { - let tasks = YamlTask::from_yaml(filename)?; - tasks.into_iter().map(|t| self.add_tasks(vec![t])).count(); - Ok(()) - } - - /// Add new tasks into dagrs - /// - /// # Example - /// ``` - /// let dagrs = DagEngine::new(); - /// dagrs.add_tasks(vec![task1, task2]); - /// dagrs.run(); - /// ``` - /// - /// Here `task1` and `task2` are user defined task wrapped in [`TaskWrapper`]. - pub fn add_tasks(&mut self, tasks: Vec) { - for task in tasks { - self.tasks.insert(task.get_id(), Arc::new(task)); - } - } - - /// Push a task's [`ExecState`] into hash store - fn push_execstate(&mut self, id: usize, state: ExecState) { - assert!( - !self.execstate_store.contains_key(&id), - "[Error] Repetitive push execstate, id: {}", - id - ); - self.execstate_store.insert(id, state); - } - - /// Fetch a task's [`ExecState`], this won't delete it from the hash map. - fn pull_execstate(&self, id: &usize) -> &ExecState { - self.execstate_store - .get(id) - .expect("[Error] Pull execstate fails") - } - - /// Prepare a task's [`Inputval`]. - fn form_input(&self, id: &usize) -> Inputval { - let froms = self.tasks[id].get_input_from_list(); - Inputval::new( - froms - .iter() - .map(|from| self.pull_execstate(from).get_dmap()) - .collect(), - ) - } - - /// create rely map between tasks. - /// - /// This operation will initialize `dagrs.rely_graph` if no error occurs. - fn create_graph(&mut self) -> Result<(), DagError> { - let size = self.tasks.len(); - self.rely_graph.set_graph_size(size); - - // Add Node (create id - index mapping) - self.tasks - .iter() - .map(|(&n, _)| self.rely_graph.add_node(n)) - .count(); - - // Form Graph - for (&id, task) in self.tasks.iter() { - let index = self.rely_graph.find_index_by_id(&id).unwrap(); - - for rely_task_id in task.get_exec_after_list() { - // Rely task existence check - let rely_index = self.rely_graph.find_index_by_id(&rely_task_id).ok_or( - DagError::running_error(RunningError::RelyTaskIllegal(task.get_name())), - )?; - - self.rely_graph.add_edge(rely_index, index); - } - } - - Ok(()) - } - - /// Check whether it's DAG or not. - /// - /// If it is a DAG, dagrs will start executing tasks in a feasible order and - /// return true when execution done, or it return a false. - async fn check_dag(&mut self) -> bool { - if let Some(seq) = self.rely_graph.topo_sort() { - let seq = seq - .into_iter() - .map(|index| self.rely_graph.find_id_by_index(index).unwrap()) - .collect(); - self.print_seq(&seq); - - // Start Executing - for id in seq { - info!("Executing Task[name: {}]", self.tasks[&id].get_name()); - - let input = self.form_input(&id); - let env = self.env.clone(); - - let task = self.tasks[&id].clone(); - let handle = tokio::spawn(async move { task.run(input, env) }); - - // Recore executing state. - let state = if let Ok(val) = handle.await { - ExecState::new(true, val) - } else { - ExecState::new(false, Retval::empty()) - }; - - info!( - "Finish Task[name: {}], success: {}", - self.tasks[&id].get_name(), - state.success() - ); - // Push executing state in to store. - self.push_execstate(id, state); - } - - true - } else { - error!("Loop Detect"); - false - } - } - - /// Print possible execution sequnces. - fn print_seq(&self, seq: &Vec) { - let mut res = String::from("[Start]"); - seq.iter() - .map(|id| res.push_str(&format!(" -> {}", self.tasks[id].get_name()))) - .count(); - info!("{} -> [End]", res); - } -} diff --git a/dagrs/src/engine/env_variables.rs b/dagrs/src/engine/env_variables.rs deleted file mode 100644 index 0ebd81cb..00000000 --- a/dagrs/src/engine/env_variables.rs +++ /dev/null @@ -1,59 +0,0 @@ -//! Implementation for global environment variables. - -use crate::task::DMap; -use anymap::CloneAny; -use std::{ - collections::HashMap, - sync::{Arc, Mutex}, -}; - -/// Global environment variables. -/// -/// Since it will be shared between tasks, [`Arc`] and [`Mutex`] -/// are needed. -pub struct EnvVar(Arc>>); - -impl EnvVar { - /// Allocate a new [`EnvVar`]. - pub fn new() -> Self { - Self(Arc::new(Mutex::new(HashMap::new()))) - } - - #[allow(unused)] - /// Set a gloval variables. - /// - /// # Example - /// ```rust - /// env.set("Hello", "World".to_string()); - /// ``` - /// - /// Lock operations are wrapped inside, so no need to worry. - pub fn set(&mut self, name: &str, var: H) { - let mut v = DMap::new(); - v.insert(var); - self.0.lock().unwrap().insert(name.to_owned(), v); - } - - #[allow(unused)] - /// This method get needed input value from [`Inputval`]. - /// - /// # Example - /// ```rust - /// env.set("Hello", "World".to_string()); - /// let res = env.get("Hello").unwrap(); - /// assert_eq!(res, "World".to_string()); - /// ``` - pub fn get(&self, name: &str) -> Option { - if let Some(dmap) = self.0.lock().unwrap().get(name) { - dmap.clone().remove() - } else { - None - } - } -} - -impl Clone for EnvVar { - fn clone(&self) -> Self { - Self(Arc::clone(&self.0)) - } -} \ No newline at end of file diff --git a/dagrs/src/engine/error.rs b/dagrs/src/engine/error.rs new file mode 100644 index 00000000..6c6805b2 --- /dev/null +++ b/dagrs/src/engine/error.rs @@ -0,0 +1,38 @@ +//! Errors that may be raised by building and running dag jobs. + +use thiserror::Error; + +use crate::parser::ParserError; +use crate::task::RunningError; + +#[derive(Debug, Error)] +/// A synthesis of all possible errors. +pub enum DagError { + /// Error that occurs when running action. + #[error("{0}")] + RunningError(RunningError), + /// Yaml file parsing error. + #[error("{0}")] + YamlParserError(ParserError), + /// Task dependency error. + #[error("Task[{0}] dependency task not exist.")] + RelyTaskIllegal(String), + /// There are loops in task dependencies. + #[error("Illegal directed a cyclic graph, loop Detect!")] + LoopGraph, + /// There are no tasks in the job. + #[error("There are no tasks in the job.")] + EmptyJob, +} + +impl From for DagError { + fn from(value: ParserError) -> Self { + Self::YamlParserError(value) + } +} + +impl From for DagError { + fn from(value: RunningError) -> Self { + Self::RunningError(value) + } +} diff --git a/dagrs/src/engine/error_handler.rs b/dagrs/src/engine/error_handler.rs deleted file mode 100644 index d47c3157..00000000 --- a/dagrs/src/engine/error_handler.rs +++ /dev/null @@ -1,76 +0,0 @@ -//! A simple error handler implementation. - -use thiserror::Error; - -#[derive(Debug, Error)] -/// A synthesis of all possible errors. -pub enum DagError { - /// IO Error, like file not exist, etc. - #[error("{0}")] - IOError(#[from] std::io::Error), - /// YAML Error, like format error, etc. - #[error("{0}")] - YamlError(YamlError), - /// Error that occurs when running dagrs. - #[error("{0}")] - RunningError(RunningError), -} - -#[derive(Debug, Error)] -/// Format Error, point out which part has what kinds of error. -pub enum YamlFormatError { - #[error("Not start with 'dagrs'")] - StartWordError, - #[error("Task[{0}] has no name field")] - NoName(String), - #[error("Task[{0}] run script format error")] - RunScriptError(String) -} - -#[derive(Debug, Error)] -/// Error that occurs when parsing YAML file. -pub enum YamlError { - #[error("{0}")] - YamlParserError(#[from] yaml_rust::ScanError), - #[error("{0}")] - YamlFormatError(YamlFormatError), -} - -#[derive(Debug, Error)] -/// Error that occurs when running dagrs -pub enum RunningError { - #[error("Task[{0}] dependency task not exist")] - RelyTaskIllegal(String), - #[error("Task[{0}] run script fails, details: {1}")] - RunScriptFailure(String, String) -} - -impl DagError { - /// Throw a format error - /// - /// # Example - /// ``` - /// DagError::format_error(YamlFormatError::NoName("a".to_string())); - /// ``` - /// This will throw a error that says, yaml task 'a' has no name field. - pub fn format_error(error: YamlFormatError) -> Self { - Self::YamlError(YamlError::YamlFormatError(error)) - } - - /// Throw a running error - /// - /// # Example - /// ``` - /// DagError::running_error(RunningError::RelyTaskIllegal("task 1".to_string())) - /// ``` - /// This will throw a error that says, task with name "task 1" has non-exist rely tasks. - pub fn running_error(error: RunningError) -> Self { - Self::RunningError(error) - } -} - -impl From for DagError { - fn from(e: yaml_rust::ScanError) -> Self { - Self::YamlError(YamlError::YamlParserError(e)) - } -} \ No newline at end of file diff --git a/dagrs/src/engine/graph.rs b/dagrs/src/engine/graph.rs index 2bfa107a..23a7861a 100644 --- a/dagrs/src/engine/graph.rs +++ b/dagrs/src/engine/graph.rs @@ -1,116 +1,110 @@ -//! Graph stores dependency relations +//! Task Graph +//! +//! # Graph stores dependency relations. +//! +//! [`Graph`] represents a series of tasks with dependencies, and stored in an adjacency +//! list. It must be a directed acyclic graph, that is, the dependencies of the task +//! cannot form a loop, otherwise the engine will not be able to execute the task successfully. +//! It has some useful methods for building graphs, such as: adding edges, nodes, etc. +//! And the most important of which is the `topo_sort` function, which uses topological +//! sorting to generate the execution sequence of tasks. +//! +//! # An example of a directed acyclic graph +//! +//! task1 -→ task3 ---→ task6 ---- +//! | ↗ ↓ ↓ ↘ +//! | / task5 ---→ task7 ---→ task9 +//! ↓ / ↑ ↓ ↗ +//! task2 -→ task4 ---→ task8 ---- +//! +//! The task execution sequence can be as follows: +//! task1->task2->task3->task4->task5->task6->task7->task8->task9 +//! use bimap::BiMap; #[derive(Debug)] /// Graph Struct -pub struct Graph { +pub(crate) struct Graph { size: usize, - /// Record node id and it's index + /// Record node id and it's index nodes: BiMap, /// Adjacency list adj: Vec>, - /// Node's indegree, used for topo sort - indegree: Vec, + /// Node's in_degree, used for topological sort + in_degree: Vec, } impl Graph { /// Allocate an empty graph - /// - /// # Example - /// ``` - /// let g = Grapg::new(); - /// ``` - pub fn new() -> Graph { + pub(crate) fn new() -> Graph { Graph { size: 0, nodes: BiMap::new(), adj: Vec::new(), - indegree: Vec::new(), + in_degree: Vec::new(), } } /// Set graph size, size is the number of tasks - /// - /// # Example - /// ``` - /// let size = 10; // 10 nodes - /// g.set_graph_size(size); - /// ``` - pub fn set_graph_size(&mut self, size: usize) { + pub(crate) fn set_graph_size(&mut self, size: usize) { self.size = size; self.adj.resize(size, Vec::new()); - self.indegree.resize(size, 0) + self.in_degree.resize(size, 0) } /// Add a node into the graph - /// /// This operation will create a mapping between ID and its index. - /// - /// # Example - /// ``` - /// g.add_node("Node1"); - /// ``` /// **Note:** `id` won't get repeated in dagrs, /// since yaml parser will overwrite its info if a task's ID repeats. - pub fn add_node(&mut self, id: usize) { + pub(crate) fn add_node(&mut self, id: usize) { let index = self.nodes.len(); self.nodes.insert(id, index); } /// Add an edge into the graph. - /// - /// # Example - /// ``` - /// g.add_edge(0, 1); - /// ``` /// Above operation adds a arrow from node 0 to node 1, /// which means task 0 shall be executed before task 1. - pub fn add_edge(&mut self, v: usize, w: usize) { + pub(crate) fn add_edge(&mut self, v: usize, w: usize) { self.adj[v].push(w); - self.indegree[w] += 1; + self.in_degree[w] += 1; } /// Find a task's index by its ID - pub fn find_index_by_id(&self, id: &usize) -> Option { + pub(crate) fn find_index_by_id(&self, id: &usize) -> Option { self.nodes.get_by_left(id).map(|i| i.to_owned()) } /// Find a task's ID by its index - pub fn find_id_by_index(&self, index: usize) -> Option { + pub(crate) fn find_id_by_index(&self, index: usize) -> Option { self.nodes.get_by_right(&index).map(|n| n.to_owned()) } - /// Do topo sort in graph, returns a possible execution sequnce if DAG - /// - /// # Example - /// ``` - /// g.topo_sort(); - /// ``` - /// This operation will judge whether graph is a DAG or not, + /// Do topo sort in graph, returns a possible execution sequence if DAG. + /// This operation will judge whether graph is a DAG or not, /// returns Some(Possible Sequence) if yes, and None if no. - /// - /// + /// + /// /// **Note**: this function can only be called after graph's initialization (add nodes and edges, etc.) is done. - /// + /// /// # Principle /// Reference: [Topological Sorting](https://www.jianshu.com/p/b59db381561a) - /// - /// 1. For a grapg g, we record the indgree of every node. - /// - /// 2. Each time we start from a node with zero indegree, name it N0, and N0 can be executed since it has no dependency. - /// - /// 3. And then we decrease the indegree of N0's children (those tasks depend on N0), this would create some new zero indegree nodes. - /// + /// + /// 1. For a graph g, we record the in-degree of every node. + /// + /// 2. Each time we start from a node with zero in-degree, name it N0, and N0 can be executed since it has no dependency. + /// + /// 3. And then we decrease the in-degree of N0's children (those tasks depend on N0), this would create some new zero in-degree nodes. + /// /// 4. Just repeat step 2, 3 until no more zero degree nodes can be generated. /// If all tasks have been executed, then it's a DAG, or there must be a loop in the graph. - pub fn topo_sort(&self) -> Option> { + pub(crate) fn topo_sort(&self) -> Option> { let mut queue = Vec::new(); - let mut indegree = self.indegree.clone(); + let mut in_degree = self.in_degree.clone(); let mut count = 0; let mut sequence = vec![]; - indegree + in_degree .iter() .enumerate() .map(|(index, °ree)| { @@ -121,7 +115,7 @@ impl Graph { .count(); while !queue.is_empty() { - let v = queue.pop().unwrap(); // This unwrap is ok since `queue` is not empty + let v = queue.pop().unwrap(); // This unwrap is ok since `queue` is not empty sequence.push(v); count += 1; @@ -129,8 +123,8 @@ impl Graph { self.adj[v] .iter() .map(|&index| { - indegree[index] -= 1; - if indegree[index] == 0 { + in_degree[index] -= 1; + if in_degree[index] == 0 { queue.push(index) } }) @@ -143,4 +137,23 @@ impl Graph { Some(sequence) } } + + /// Get the out degree of a node. + pub(crate) fn get_node_out_degree(&self, id: &usize) -> usize { + match self.nodes.get_by_left(id) { + Some(index) => self.adj[*index].len(), + None => 0, + } + } +} + +impl Default for Graph { + fn default() -> Self { + Graph { + size: 0, + nodes: BiMap::new(), + adj: Vec::new(), + in_degree: Vec::new(), + } + } } diff --git a/dagrs/src/engine/mod.rs b/dagrs/src/engine/mod.rs index ffc5efc3..d6dc99ea 100644 --- a/dagrs/src/engine/mod.rs +++ b/dagrs/src/engine/mod.rs @@ -1,8 +1,98 @@ -mod dag_engine; -mod error_handler; +//! The Engine +//! +//! # dagrs core logic. +//! +//! [`Dag`] consists of a series of executable tasks with dependencies. A Dag can be executed +//! alone as a job. We can get the execution result and execution status of dag. +//! [`Engine`] can manage multiple [`Dag`]. An Engine can consist of multiple Dags of different +//! types of tasks. For example, you can give a Dag in the form of a yaml configuration file, +//! then give a Dag in the form of a custom configuration file, and finally give it in a programmatic way. +//! [`Engine`] stores each Dag in the form of a key-value pair (), and the user +//! can specify which task to execute by giving the name of the Dag, or follow the order in which +//! the Dags are added to the Engine , executing each Dag in turn. + +pub use dag::Dag; +pub use error::DagError; + +mod dag; +mod error; mod graph; -mod env_variables; -pub use error_handler::*; -pub use dag_engine::DagEngine; -pub use env_variables::EnvVar; \ No newline at end of file +use std::collections::HashMap; + +use anymap2::any::CloneAnySendSync; +use tokio::runtime::Runtime; + +use crate::log; + +/// The Engine. Manage multiple Dags. +pub struct Engine { + dags: HashMap, + /// According to the order in which Dags are added to the Engine, assign a sequence number to each Dag. + /// Sequence numbers can be used to execute Dags sequentially. + sequence: HashMap, + /// A tokio runtime. + /// In order to save computer resources, multiple Dags share one runtime. + runtime: Runtime, +} + +impl Engine { + /// Add a Dag to the Engine and assign a sequence number to the Dag. + /// It should be noted that different Dags should specify different names. + pub fn append_dag(&mut self, name: &str, mut dag: Dag) { + if !self.dags.contains_key(name) { + match dag.init() { + Ok(()) => { + self.dags.insert(name.to_string(), dag); + let len = self.sequence.len(); + self.sequence.insert(len + 1, name.to_string()); + } + Err(err) => { + log::error(format!("Some error occur: {}", err)); + } + } + } + } + + /// Given a Dag name, execute this Dag. + /// Returns true if the given Dag executes successfully, otherwise false. + pub fn run_dag(&mut self, name: &str) -> bool { + if !self.dags.contains_key(name) { + log::error(format!("No job named '{}'", name)); + false + } else { + let job = self.dags.get(name).unwrap(); + self.runtime.block_on(job.run()) + } + } + + /// Execute all the Dags in the Engine in sequence according to the order numbers of the Dags in + /// the sequence from small to large. The return value is the execution status of all tasks. + pub fn run_sequential(&mut self) ->Vec{ + let mut res=Vec::new(); + for seq in 1..self.sequence.len() + 1 { + let name = self.sequence.get(&seq).unwrap().clone(); + res.push(self.run_dag(name.as_str())); + } + res + } + + /// Given the name of the Dag, get the execution result of the specified Dag. + pub fn get_dag_result(&self,name:&str)->Option{ + if self.dags.contains_key(name) { + self.dags.get(name).unwrap().get_result() + } else { + None + } + } +} + +impl Default for Engine { + fn default() -> Self { + Self { + dags: HashMap::new(), + runtime: Runtime::new().unwrap(), + sequence: HashMap::new(), + } + } +} diff --git a/dagrs/src/lib.rs b/dagrs/src/lib.rs index 1b430f13..21b662ca 100644 --- a/dagrs/src/lib.rs +++ b/dagrs/src/lib.rs @@ -1,151 +1,15 @@ -extern crate anymap; +extern crate anymap2; extern crate bimap; extern crate clap; -extern crate crossbeam; extern crate deno_core; -extern crate lazy_static; -extern crate log; -extern crate simplelog; extern crate yaml_rust; +pub use engine::{Dag, DagError, Engine}; +pub use parser::*; +pub use task::{Action, DefaultTask, alloc_id, Input, JavaScript, Output, RunningError, ShScript, Task, YamlTask}; +pub use utils::{EnvVar, gen_macro,LogLevel,Logger,log}; + mod engine; +mod parser; mod task; - -pub use engine::{DagEngine, DagError, EnvVar, RunningError, YamlError, YamlFormatError}; -pub use task::{Inputval, Retval, RunScript, RunType, TaskTrait, TaskWrapper}; - -use simplelog::*; -use std::{ - env, - fs::{create_dir, File}, -}; - -/// Init a logger. -/// -/// # Example -/// ```rust -/// // Default path (HOME/.dagrs/dagrs.log) -/// init_logger(None); -/// // or -/// init_logger(Some("./dagrs.log")); -/// ``` -/// -/// **Note**, this function shall only be called once. -/// -/// Default logger is [Simplelog](https://crates.io/crates/simplelog), you can -/// also use other log implementations. Just remember to initialize them before -/// running dagrs. -pub fn init_logger(logpath: Option<&str>) { - let logpath = if let Some(s) = logpath { - s.to_owned() - } else { - if let Ok(home) = env::var("HOME") { - create_dir(format!("{}/.dagrs", home)).unwrap_or(()); - format!("{}/.dagrs/dagrs.log", home) - } else { - "./dagrs.log".to_owned() - } - }; - - CombinedLogger::init(vec![ - TermLogger::new( - LevelFilter::Info, - Config::default(), - TerminalMode::Mixed, - ColorChoice::Auto, - ), - WriteLogger::new( - LevelFilter::Info, - Config::default(), - File::create(logpath).unwrap(), - ), - ]) - .unwrap(); -} - -#[test] -fn test_value_pass1() { - use crate::task::{Inputval, Retval, TaskTrait, TaskWrapper}; - struct T1 {} - impl TaskTrait for T1 { - fn run(&self, _input: Inputval, _env: EnvVar) -> Retval { - println!("T1, return 1"); - Retval::new(1i32) - } - } - - struct T2 {} - impl TaskTrait for T2 { - fn run(&self, mut input: Inputval, _env: EnvVar) -> Retval { - let val_from_t1 = input.get::(0); - println!("T2, receive: {:?}", val_from_t1); - Retval::empty() - } - } - - let t1 = TaskWrapper::new(T1 {}, "Task 1"); - let mut t2 = TaskWrapper::new(T2 {}, "Task 2"); - - t2.exec_after(&[&t1]); - t2.input_from(&[&t1]); - - let mut dag = DagEngine::new(); - dag.add_tasks(vec![t1, t2]); - - dag.run().unwrap(); -} - -#[test] -fn test_value_pass2() { - use crate::task::{Inputval, Retval, TaskTrait, TaskWrapper}; - struct T1 {} - impl TaskTrait for T1 { - fn run(&self, _input: Inputval, mut env: EnvVar) -> Retval { - println!("T1, return 1, set env [Hello: World]"); - env.set("Hello", "World".to_string()); - Retval::new(1i32) - } - } - - struct T2 {} - impl TaskTrait for T2 { - fn run(&self, mut input: Inputval, _env: EnvVar) -> Retval { - let val_from_t1 = input.get::(0); - println!("T2, receive from T1: {:?}, return '123'", val_from_t1); - Retval::new("123".to_string()) - } - } - - struct T3 {} - impl TaskTrait for T3 { - fn run(&self, mut input: Inputval, env: EnvVar) -> Retval { - // Order of input value is the same as the order of tasks - // passed in `input_from`. - let val_from_t1 = input.get::(0); - let val_from_t2 = input.get::(1); - let eval = env.get::("Hello"); - - println!( - "T3, receive from T1: {:?}, T2: {:?}, env: {:?}", - val_from_t1, val_from_t2, eval - ); - - Retval::empty() - } - } - - let t1 = TaskWrapper::new(T1 {}, "Task 1"); - let mut t2 = TaskWrapper::new(T2 {}, "Task 2"); - let mut t3 = TaskWrapper::new(T3 {}, "Task 3"); - - t2.exec_after(&[&t1]); - t2.input_from(&[&t1]); - - t3.exec_after(&[&t1, &t2]); - t3.input_from(&[&t1, &t2]); - - let mut dag = DagEngine::new(); - dag.add_tasks(vec![t1, t2, t3]); - - dag.run().unwrap(); -} +mod utils; \ No newline at end of file diff --git a/dagrs/src/main.rs b/dagrs/src/main.rs index eef2f19e..cff229de 100644 --- a/dagrs/src/main.rs +++ b/dagrs/src/main.rs @@ -1,152 +1,40 @@ use clap::Parser; -use dagrs::{init_logger, DagEngine}; -use log::*; - -#[derive(Parser)] -#[clap(version)] -/// Command Line input -struct Args { - /// YAML file path - file: String, - /// Log file path - #[clap(short, long)] - logpath: Option, +use dagrs::{Dag, log, LogLevel}; + +#[derive(Parser, Debug)] +#[command(name= "dagrs",version= "0.2.0")] +struct Args{ + /// Log output file, the default is to print to the terminal. + #[arg(long)] + log_path: Option, + /// yaml configuration file path. + #[arg(long)] + yaml: String, + /// Log level, the default is Info. + #[arg(long)] + log_level:Option } fn main() { let args = Args::parse(); - let dagrs: DagEngine = DagEngine::new(); - - init_logger(args.logpath.as_deref()); - - if let Err(e) = dagrs.run_from_yaml(&args.file) { - error!("[Error] {}", e); - } -} - -#[test] -fn test_dag() { - let res = DagEngine::new() - .run_from_yaml("test/test_dag2.yaml") - .unwrap(); - assert_eq!(res, true) -} - -#[test] -fn test_runscript() { - let res = DagEngine::new() - .run_from_yaml("test/test_dag1.yaml") - .unwrap(); - assert_eq!(res, true) -} - -#[test] -fn test_value_pass1() { - use std::fs::File; - use std::io::Read; - - let res = DagEngine::new() - .run_from_yaml("test/test_value_pass1.yaml") - .unwrap(); - assert_eq!(res, true); - - let mut buf = String::new(); - File::open("./test/test_value_pass1.txt") - .expect("Test Fails, File not exist.") - .read_to_string(&mut buf) - .expect("Test Fails, Read file fails."); - - assert_eq!(buf, "10\n"); -} - -#[test] -fn test_value_pass2() { - use std::fs::File; - use std::io::Read; - - let res = DagEngine::new() - .run_from_yaml("test/test_value_pass2.yaml") - .unwrap(); - assert_eq!(res, true); - - let mut buf1 = String::new(); - File::open("./test/test_value_pass2.txt") - .expect("Test Fails, File not exist.") - .read_to_string(&mut buf1) - .expect("Test Fails, Read file fails."); - - let mut buf2 = String::new(); - File::open("./README.md") - .expect("Test Fails, File not exist.") - .read_to_string(&mut buf2) - .expect("Test Fails, Read file fails."); - - assert_eq!(buf1, buf2); -} - - -#[test] -fn test_loop() { - let res = DagEngine::new() - .run_from_yaml("test/test_loop1.yaml") - .unwrap(); - assert_eq!(res, false) -} - -#[test] -fn test_complex_loop() { - let res = DagEngine::new() - .run_from_yaml("test/test_loop2.yaml") - .unwrap(); - assert_eq!(res, false) -} - -#[test] -fn test_format_error1() { - use dagrs::{DagError, YamlError, YamlFormatError}; - let res = DagEngine::new().run_from_yaml("test/test_error1.yaml"); - - assert!(matches!( - res, - Err(DagError::YamlError(YamlError::YamlFormatError( - YamlFormatError::NoName(_) - ))) - )); -} - -#[test] -fn test_format_error2() { - use dagrs::{DagError, YamlError, YamlFormatError}; - let res = DagEngine::new().run_from_yaml("test/test_error2.yaml"); - - assert!(matches!( - res, - Err(DagError::YamlError(YamlError::YamlFormatError( - YamlFormatError::StartWordError - ))) - )); -} - -#[test] -fn test_rely_error() { - use dagrs::{DagError, RunningError}; - let res = DagEngine::new().run_from_yaml("test/test_error3.yaml"); - - assert!(matches!( - res, - Err(DagError::RunningError(RunningError::RelyTaskIllegal(_))) - )); -} - -#[test] -fn test_no_runscript() { - use dagrs::{DagError, YamlError, YamlFormatError}; - let res = DagEngine::new().run_from_yaml("test/test_error4.yaml"); - - assert!(matches!( - res, - Err(DagError::YamlError(YamlError::YamlFormatError( - YamlFormatError::RunScriptError(_) - ))) - )); + let log_level=args.log_level.map_or(LogLevel::Info,|level|{ + match level.as_str() { + "debug" => {LogLevel::Debug} + "info" => {LogLevel::Info} + "warn" => {LogLevel::Warn} + "error" => {LogLevel::Error} + "off" => {LogLevel::Off} + _ => { + println!("The logging level can only be [debug,info,warn,error,off]"); + std::process::abort(); + } + } + }); + match args.log_path { + None => {log::init_logger(log_level,None)} + Some(path) => {log::init_logger(log_level,Some(std::fs::File::create(path).unwrap()))} + }; + let yaml_path=args.yaml; + let mut dag=Dag::with_yaml(yaml_path.as_str()).unwrap(); + assert!(dag.start().unwrap()); } diff --git a/dagrs/src/parser/error.rs b/dagrs/src/parser/error.rs new file mode 100644 index 00000000..56989ef4 --- /dev/null +++ b/dagrs/src/parser/error.rs @@ -0,0 +1,65 @@ +//! Errors that may occur during configuration file parsing. + +use thiserror::Error; + +/// Errors that may occur while parsing task configuration files. +#[derive(Debug, Error)] +pub enum ParserError { + /// Configuration file not found. + #[error("File not found. [{0}]")] + FileNotFound(#[from] std::io::Error), + #[error("{0}")] + YamlTaskError(YamlTaskError), + #[error("{0}")] + FileContentError(FileContentError), +} + +/// Error about file information. +#[derive(Debug, Error, PartialEq, Eq)] +pub enum FileContentError { + /// The format of the yaml configuration file is not standardized. + #[error("{0}")] + IllegalYamlContent(#[from] yaml_rust::ScanError), + /// Config file has no content. + #[error("File is empty! [{0}]")] + Empty(String), +} + +/// Errors about task configuration items. +#[derive(Debug, Error, PartialEq, Eq)] +pub enum YamlTaskError { + /// The configuration file should start with `dagrs:`. + #[error("File content is not start with 'dagrs'.")] + StartWordError, + /// No task name configured. + #[error("Task has no name field. [{0}]")] + NoNameAttr(String), + /// The specified task predecessor was not found. + #[error("Task cannot find the specified predecessor. [{0}]")] + NotFoundPrecursor(String), + /// `run` is not defined. + #[error("The 'run' attribute is not defined. [{0}]")] + NoRunAttr(String), + /// `type` is not defined. + #[error("The 'type' attribute is not defined. [{0}]")] + NoTypeAttr(String), + /// Unsupported script type. + #[error("Unsupported script type [{0}]")] + UnsupportedType(String), + /// `script` is not defined. + #[error("The 'script' attribute is not defined. [{0}]")] + NoScriptAttr(String), +} + +impl From for ParserError { + fn from(value: FileContentError) -> Self { + ParserError::FileContentError(value) + } +} + +impl From for ParserError { + fn from(value: YamlTaskError) -> Self { + ParserError::YamlTaskError(value) + } +} + diff --git a/dagrs/src/parser/mod.rs b/dagrs/src/parser/mod.rs new file mode 100644 index 00000000..59e45a6a --- /dev/null +++ b/dagrs/src/parser/mod.rs @@ -0,0 +1,81 @@ +//! Parsing configuration files. +//! +//! # Config file parser +//! +//! When users customize configuration files, the program needs to use the configuration +//! file parser defined by this module. The parser is responsible for parsing the content +//! defined in the configuration file into a series of tasks with dependencies. +//! +//! The program provides a default Yaml configuration file parser: [`YamlParser`]. However, +//! users are allowed to customize the parser, which requires the user to implement the [`Parser`] trait. +//! Currently, the program only supports configuration files in *.yaml format, and may support +//! configuration files in *.json format in the future. +//! +//! # The basic format of the yaml configuration file is as follows: +//! ```yaml +//! dagrs: +//! a: +//! name: "Task 1" +//! after: [b, c] +//! run: +//! type: sh +//! script: echo a +//! b: +//! name: "Task 2" +//! after: [c, f, g] +//! run: +//! type: sh +//! script: echo b +//! c: +//! name: "Task 3" +//! after: [e, g] +//! run: +//! type: sh +//! script: echo c +//! d: +//! name: "Task 4" +//! after: [c, e] +//! run: +//! type: sh +//! script: echo d +//! e: +//! name: "Task 5" +//! after: [h] +//! run: +//! type: sh +//! script: echo e +//! f: +//! name: "Task 6" +//! after: [g] +//! run: +//! type: deno +//! script: Deno.core.print("f\n") +//! g: +//! name: "Task 7" +//! after: [h] +//! run: +//! type: deno +//! script: Deno.core.print("g\n") +//! h: +//! name: "Task 8" +//! run: +//! type: sh +//! script: echo h +//! ``` +//! +//! Currently yaml configuration files support two types of tasks, sh and javascript. + +pub use error::*; +pub use yaml_parser::YamlParser; + +use crate::task::Task; + +mod error; +mod yaml_parser; + +/// Generic parser traits. If users want to customize the configuration file parser, they must implement this trait. +/// [`YamlParser`] is an example of [`Parser`] +pub trait Parser { + /// Parses the contents of a configuration file into a series of tasks with dependencies. + fn parse_tasks(&self, file: &str) -> Result>, ParserError>; +} diff --git a/dagrs/src/parser/yaml_parser.rs b/dagrs/src/parser/yaml_parser.rs new file mode 100644 index 00000000..b0d4c3dd --- /dev/null +++ b/dagrs/src/parser/yaml_parser.rs @@ -0,0 +1,125 @@ +//! Default yaml configuration file parser. + +use std::{collections::HashMap, fs::File, io::Read}; + +use yaml_rust::{Yaml, YamlLoader}; + +use crate::task::{JavaScript, ShScript, Task, YamlTask}; + +use super::{ + error::{FileContentError, ParserError, YamlTaskError}, + Parser, +}; + +/// An implementation of [`Parser`]. It is the default yaml configuration file parser. +pub struct YamlParser; + +impl YamlParser { + /// Given file path, and load configuration file. + fn load_file(&self, file: &str) -> Result { + let mut content = String::new(); + let mut yaml = File::open(file)?; + yaml.read_to_string(&mut content).unwrap(); + Ok(content) + } + /// Parses an item in the configuration file into a task. + /// An item refers to: + /// + /// ```yaml + /// name: "Task 1" + /// after: [b, c] + /// run: + /// type: sh + /// script: echo a + /// ``` + fn parse_one(&self, id: &str, item: &Yaml) -> Result { + // Get name first + let name = item["name"] + .as_str() + .ok_or(YamlTaskError::NoNameAttr(id.to_owned()))? + .to_owned(); + // precursors can be empty + let mut precursors = Vec::new(); + if let Some(after_tasks) = item["after"].as_vec() { + after_tasks + .iter() + .map(|task_id| precursors.push(task_id.as_str().unwrap().to_owned())) + .count(); + } + // Get run script + let run = &item["run"]; + if run.is_badvalue() { + return Err(YamlTaskError::NoRunAttr(name)); + } + match run["type"] + .as_str() + .ok_or(YamlTaskError::NoTypeAttr(name.clone()))? + { + "sh" => { + let sh_script = run["script"] + .as_str() + .ok_or(YamlTaskError::NoScriptAttr(name.clone()))?; + Ok(YamlTask::new( + id, + precursors, + name, + ShScript::new(sh_script), + )) + } + "deno" => { + let js_script = run["script"] + .as_str() + .ok_or(YamlTaskError::NoScriptAttr(name.clone()))?; + Ok(YamlTask::new( + id, + precursors, + name, + JavaScript::new(js_script), + )) + } + _ => Err(YamlTaskError::UnsupportedType(name)), + } + } +} + +impl Parser for YamlParser { + fn parse_tasks(&self, file: &str) -> Result>, ParserError> { + let content = self.load_file(file)?; + // Parse Yaml + let yaml_tasks = YamlLoader::load_from_str(&content) + .map_err(FileContentError::IllegalYamlContent)?; + // empty file error + if yaml_tasks.is_empty() { + return Err(FileContentError::Empty(file.to_string()).into()); + } + let yaml_tasks = yaml_tasks[0]["dagrs"] + .as_hash() + .ok_or(YamlTaskError::StartWordError)?; + let mut tasks = Vec::new(); + let mut map = HashMap::new(); + // Read tasks + for (v, w) in yaml_tasks { + let id = v.as_str().unwrap(); + let task = self.parse_one(id, w)?; + map.insert(id, task.id()); + tasks.push(task); + } + + for task in tasks.iter_mut() { + let mut pres = Vec::new(); + for pre in task.str_precursors() { + if map.contains_key(&pre[..]) { + pres.push(map[&pre[..]]); + } else { + return Err(YamlTaskError::NotFoundPrecursor(task.name()).into()); + } + } + task.init_precursors(pres); + } + + Ok(tasks + .into_iter() + .map(|task| Box::new(task) as Box) + .collect()) + } +} diff --git a/dagrs/src/task/error.rs b/dagrs/src/task/error.rs new file mode 100644 index 00000000..821ad951 --- /dev/null +++ b/dagrs/src/task/error.rs @@ -0,0 +1,72 @@ +//! There is an error about task runtime. + +use std::fmt::Display; + +use thiserror::Error; + +/// Errors that may be generated when the specific behavior of the task is run. +/// This is just a simple error handling. When running the tasks in the configuration file, +/// some errors can be found by the user, which is convenient for debugging. +/// It also allows users to return expected errors in custom task behavior. However, even +/// if this error is expected, it will cause the execution of the entire task to fail. +#[derive(Debug)] +pub struct RunningError { + msg: String, +} + +/// Sh script produces incorrect behavior when run. +#[derive(Error, Debug)] +pub struct ShExecuteError { + msg: String, +} + +/// Javascript script produces incorrect behavior when run. +#[derive(Error, Debug)] +pub enum JavaScriptExecuteError { + #[error("{0}")] + AnyHowError(deno_core::anyhow::Error), + #[error("{0}")] + SerializeError(deno_core::serde_v8::Error), +} + +impl RunningError { + pub fn new(msg: String) -> Self { + Self { msg } + } + pub fn from_err(err: T) -> Self { + Self { + msg: err.to_string(), + } + } +} + +impl Display for RunningError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.msg) + } +} + +impl ShExecuteError { + pub fn new(msg: String) -> Self { + Self { msg } + } +} + +impl Display for ShExecuteError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "sh script execution error: {}", self.msg) + } +} + +impl From for RunningError { + fn from(value: ShExecuteError) -> Self { + RunningError { msg: value.to_string() } + } +} + +impl From for RunningError { + fn from(value: JavaScriptExecuteError) -> Self { + RunningError { msg: value.to_string() } + } +} + diff --git a/dagrs/src/task/mod.rs b/dagrs/src/task/mod.rs index 9727315b..29581c44 100644 --- a/dagrs/src/task/mod.rs +++ b/dagrs/src/task/mod.rs @@ -1,8 +1,277 @@ -pub use self::task::*; -pub use self::yaml_task::YamlTask; -pub use self::state::Retval; -pub use self::state::{Inputval, ExecState, DMap}; - -mod task; -mod yaml_task; -mod state; \ No newline at end of file +//! Relevant definitions of tasks. +//! +//! # Task execution mode of the Dag engine +//! +//! Currently, the Dag execution engine has two execution modes: +//! The first mode is to execute tasks through user-written yaml configuration file, +//! and then hand them over to the dag engine for execution. Currently, the yaml +//! configuration file supports two types of tasks, one is to execute sh scripts, +//! and the other is to execute javascript scripts. +//! +//!# The basic format of the yaml configuration file is as follows: +//! ```yaml +//! dagrs: +//! a: +//! name: "Task 1" +//! after: [b, c] +//! run: +//! type: sh +//! script: echo a +//! b: +//! name: "Task 2" +//! after: [c, f, g] +//! run: +//! type: sh +//! script: echo b +//! c: +//! name: "Task 3" +//! after: [e, g] +//! run: +//! type: sh +//! script: echo c +//! d: +//! name: "Task 4" +//! after: [c, e] +//! run: +//! type: sh +//! script: echo d +//! e: +//! name: "Task 5" +//! after: [h] +//! run: +//! type: sh +//! script: echo e +//! f: +//! name: "Task 6" +//! after: [g] +//! run: +//! type: deno +//! script: Deno.core.print("f\n") +//! g: +//! name: "Task 7" +//! after: [h] +//! run: +//! type: deno +//! script: Deno.core.print("g\n") +//! h: +//! name: "Task 8" +//! run: +//! type: sh +//! script: sh_script.sh +//! ``` +//! The necessary attributes for tasks in the yaml configuration file are: +//! id: unique identifier, such as 'a' +//! name: task name +//! after: Indicates which task to execute after, this attribute is optional +//! run: +//! type: sh or deno +//! script: command or file path +//! +//! +//! The second mode is that the user program defines the task, which requires the +//! user to implement the [`Action`] trait of the task module and rewrite the +//! run function. +//! +//! # Example +//! +//! ```rust +//! use dagrs::{Action,EnvVar,Output,RunningError,Input}; +//! use std::sync::Arc; +//! struct SimpleAction{ +//! limit:u32 +//! } +//! +//! impl Action for SimpleAction{ +//! fn run(&self, input: Input,env:Arc) -> Result { +//! let mut sum=0; +//! for i in 0..self.limit{ +//! sum+=i; +//! } +//! Ok(Output::new(sum)) +//! } +//! } +//! +//! ``` +//! +//! # Task definition. +//! +//! Currently, two different execution modes correspond to two different task types, +//! namely [`DefaultTask`] and [`YamlTask`]. +//! When users program to implement task logic, the engine uses [`DefaultTask`]; +//! When the user provides the yaml configuration file, the internal engine uses [`YamlTask`]; +//! +//! These two task types both implement the [`Task`] trait, that is to say, users can also +//! customize tasks and assign more functions and attributes to tasks. However, a task must +//! have four fixed properties (the four standard properties contained in DefaultTask): +//! - id: use [`ID_ALLOCATOR`] to get a global task unique identifier, the type must be `usize` +//! - name: the task name specified by the user, the type must be `String` +//! - predecessor_tasks: the predecessor task of this task, the type must be `Vec` +//! - action: the specific behavior to be performed by the task, the type must be `Arc` +//! +//! If users want to customize Task, they can refer to the implementation of these two specific [`Task`]. + +use std::fmt::Debug; +use std::sync::Arc; +use std::sync::atomic::AtomicUsize; + +use crate::utils::EnvVar; + +pub use self::error::{RunningError,JavaScriptExecuteError,ShExecuteError}; +pub use self::script::{JavaScript,ShScript}; +pub use self::specific_task::{YamlTask}; +pub use self::state::{Output,Input}; +pub(crate) use self::state::ExecState; + +mod error; +mod script; +mod specific_task; +mod state; + +/// Action Trait. +/// [`Action`] represents the specific behavior to be executed. +pub trait Action { + /// The specific behavior to be performed by the task. + fn run(&self, input: Input, env: Arc) -> Result; +} + +/// Tasks can have many attributes, among which `id`, `name`, `predecessor_tasks`, and +/// `runnable` attributes are required, and users can also customize some other attributes. +/// [`DefaultTask`] in this module is a [ `Task`], the DAG engine uses it as the basic +/// task by default. +/// +/// A task must provide methods to obtain precursors and required attributes, just as +/// the methods defined below, users who want to customize tasks must implement these methods. +pub trait Task { + /// Get a reference to an executable action. + fn action(&self) -> Arc; + /// Get the id of all predecessor tasks of this task. + fn predecessors(&self) -> &[usize]; + /// Get the id of this task. + fn id(&self) -> usize; + /// Get the name of this task. + fn name(&self) -> String; +} + +impl Debug for dyn Task { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "{},\t{},\t{:?}", self.id(),self.name(),self.predecessors()) + } +} + +/// A default implementation of the Task trait. In general, use it to define the tasks of dagrs. +pub struct DefaultTask { + /// id is the unique identifier of each task, it will be assigned by the global [`IDAllocator`] + /// when creating a new task, you can find this task through this identifier. + id: usize, + /// The task's name. + name: String, + /// Id of the predecessor tasks. + predecessor_tasks: Vec, + /// Perform specific actions. + action: Arc, +} + +impl DefaultTask { + /// Allocate a new [`DefaultTask`], the specific task behavior is a structure that implements [`Action`]. + /// + /// # Example + /// + /// ```rust + /// use dagrs::{DefaultTask, Output,Input, Action,EnvVar,RunningError}; + /// use std::sync::Arc; + /// struct SimpleAction(usize); + /// + /// impl Action for SimpleAction { + /// fn run(&self, input: Input, env: Arc) -> Result { + /// Ok(Output::new(self.0 + 10)) + /// } + /// } + /// + /// let action = SimpleAction(10); + /// let task = DefaultTask::new(action, "Increment action"); + /// ``` + /// + /// `SimpleAction` is a struct that impl [`Action`]. Since task will be + /// executed in separated threads, [`Send`] and [`Sync`] is needed. + /// + /// **Note:** This method will take the ownership of struct that impl [`Action`]. + pub fn new(action: impl Action + 'static + Send + Sync, name: &str) -> Self { + DefaultTask { + id: ID_ALLOCATOR.alloc(), + action: Arc::new(action), + name: name.to_owned(), + predecessor_tasks: Vec::new(), + } + } + + /// Tasks that shall be executed before this one. + /// + /// # Example + /// ```rust + /// use dagrs::{Action,DefaultTask,Input,Output,RunningError,EnvVar}; + /// use std::sync::Arc; + /// struct SimpleAction {}; + /// impl Action for SimpleAction { + /// fn run(&self, input: Input, env: Arc) -> Result { + /// Ok(Output::empty()) + /// } + /// } + /// let mut t1 = DefaultTask::new(SimpleAction{}, "Task 1"); + /// let mut t2 = DefaultTask::new(SimpleAction{}, "Task 2"); + /// t2.set_predecessors(&[&t1]); + /// ``` + /// In above code, `t1` will be executed before `t2`. + pub fn set_predecessors(&mut self, predecessors: &[&DefaultTask]) { + self.predecessor_tasks + .extend(predecessors.iter().map(|t| t.id())) + } + + /// The same as `exec_after`, but input are tasks' ids + /// rather than reference to [`DefaultTask`]. + pub fn set_predecessors_by_id(&mut self, predecessors_id: &[usize]) { + self.predecessor_tasks.extend(predecessors_id) + } +} + +impl Task for DefaultTask { + fn action(&self) -> Arc { + self.action.clone() + } + + fn predecessors(&self) -> &[usize] { + &self.predecessor_tasks + } + + fn id(&self) -> usize { + self.id + } + fn name(&self) -> String { + self.name.clone() + } +} + +/// IDAllocator for DefaultTask +struct IDAllocator { + id: AtomicUsize, +} + +impl IDAllocator { + fn alloc(&self) -> usize { + let origin = self.id.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + if origin > self.id.load(std::sync::atomic::Ordering::Relaxed) { + panic!("Too many tasks.") + } else { + origin + } + } +} + +/// The global task uniquely identifies an instance of the allocator. +static ID_ALLOCATOR: IDAllocator = IDAllocator { + id: AtomicUsize::new(1), +}; + +/// public function to assign task's id. +pub fn alloc_id()->usize{ + ID_ALLOCATOR.alloc() +} \ No newline at end of file diff --git a/dagrs/src/task/script.rs b/dagrs/src/task/script.rs new file mode 100644 index 00000000..6b6050ee --- /dev/null +++ b/dagrs/src/task/script.rs @@ -0,0 +1,92 @@ +//! Specific task. +//! +//! # Two specific types of tasks offered to users. +//! +//! One is to execute sh script tasks, and the other is to execute Javascript script tasks. +//! Both of them implement the [`Action`] trait. + +use std::{process::Command, sync::Arc}; + +use deno_core::{serde_json, serde_v8, v8, JsRuntime, RuntimeOptions}; + +use crate::{log, utils::EnvVar}; + +use super::{Action, Input, JavaScriptExecuteError, Output, RunningError, ShExecuteError}; + +/// Can be used to run a sh script. +pub struct ShScript { + script: String, +} + +/// Can be used to execute javascript scripts. +pub struct JavaScript { + script: String, +} + +impl ShScript { + pub fn new(script: &str) -> Self { + Self { + script: script.to_owned(), + } + } +} + +impl Action for ShScript { + fn run(&self, input: Input, _env: Arc) -> Result { + let args: Vec = input + .get_iter() + .map(|input| input.get::()) + .filter(|input| input.is_some()) + .map(|input| input.unwrap().clone()) + .collect(); + let out = Command::new("sh") + .arg("-c") + .arg(&self.script) + .args(args) + .output() + .unwrap(); + if !out.stderr.is_empty() { + let err_msg = String::from_utf8(out.stderr).unwrap(); + log::error(err_msg.clone()); + Err(ShExecuteError::new(err_msg).into()) + } else { + Ok(Output::new(String::from_utf8(out.stdout).unwrap())) + } + } +} + +impl JavaScript { + pub fn new(script: &str) -> Self { + Self { + script: script.to_owned(), + } + } +} + +impl Action for JavaScript { + fn run(&self, _input: Input, _env: Arc) -> Result { + let script = self.script.clone().into_boxed_str(); + let mut context = JsRuntime::new(RuntimeOptions { + ..Default::default() + }); + match context.execute_script("", deno_core::FastString::Owned(script)) { + Ok(global) => { + let scope = &mut context.handle_scope(); + let local = v8::Local::new(scope, global); + match serde_v8::from_v8::(scope, local) { + Ok(value) => Ok(Output::new(value.to_string())), + Err(err) => { + let e = JavaScriptExecuteError::SerializeError(err); + log::error(format!("JavaScript script task execution failed! {}", e)); + Err(e.into()) + } + } + } + Err(err) => { + let e = JavaScriptExecuteError::AnyHowError(err); + log::error(format!("JavaScript script task parsing failed! {}", e)); + Err(e.into()) + } + } + } +} diff --git a/dagrs/src/task/specific_task.rs b/dagrs/src/task/specific_task.rs new file mode 100644 index 00000000..4b32dbae --- /dev/null +++ b/dagrs/src/task/specific_task.rs @@ -0,0 +1,70 @@ +//! Task definition of type Yaml. +//! +//! # The task type corresponding to the configuration file: [`YamlTask`] +//! +//! [`YamlTask`] implements the [`Task`] trait, which represents the tasks in the yaml +//! configuration file, and a yaml configuration file will be parsed into a series of [`YamlTask`]. +//! It is different from [`DefaultTask`], in addition to the four mandatory attributes of the +//! task type, he has several additional attributes. + +use std::sync::Arc; + +use super::{Action, ID_ALLOCATOR, Task}; + +/// Task struct for yaml file. +pub struct YamlTask { + /// `tid.0` is the unique identifier defined in yaml, and `tid.1` is the id assigned by the global id assigner. + tid: (String, usize), + name: String, + /// Precursor identifier defined in yaml. + precursors: Vec, + precursors_id: Vec, + action: Arc, +} + +impl YamlTask { + pub fn new( + yaml_id: &str, + precursors: Vec, + name: String, + action: impl Action + Send + Sync + 'static, + ) -> Self { + Self { + tid: (yaml_id.to_owned(), ID_ALLOCATOR.alloc()), + name, + precursors, + precursors_id: Vec::new(), + action: Arc::new(action), + } + } + /// After the configuration file is parsed, the id of each task has been assigned. + /// At this time, the `precursors_id` of this task will be initialized according to + /// the id of the predecessor task of each task. + pub fn init_precursors(&mut self, pres_id: Vec) { + self.precursors_id = pres_id; + } + + /// Get the precursor identifier defined in yaml. + pub fn str_precursors(&self) -> Vec { + self.precursors.clone() + } + /// Get the unique ID of the task defined in yaml. + pub fn str_id(&self) -> String { + self.tid.0.clone() + } +} + +impl Task for YamlTask { + fn action(&self) -> Arc { + self.action.clone() + } + fn predecessors(&self) -> &[usize] { + &self.precursors_id + } + fn id(&self) -> usize { + self.tid.1 + } + fn name(&self) -> String { + self.name.clone() + } +} diff --git a/dagrs/src/task/state.rs b/dagrs/src/task/state.rs index 5bc1b474..1579ea45 100644 --- a/dagrs/src/task/state.rs +++ b/dagrs/src/task/state.rs @@ -1,111 +1,130 @@ -use std::slice::Iter; +//! Task state +//! +//! # Input, output, and state of tasks. +//! +//! [`Output`] represents the output generated when the task completes successfully. +//! The user can use the `new` function to construct an [`Output`] representing the +//! generated output, and use the `empty` function to construct an empty [`Output`] +//! if the task does not generate output. +//! +//! [`Input`] represents the input required by the task, and the input comes from the +//! output produced by multiple predecessor tasks of this task. Users can read and +//! write the content of [`Input`] ([`Input`] is actually constructed by cloning multiple +//! [`Output`]), so as to realize the logic of the program. +//! +//! [`ExeState`] internally stores [`]Output`], which represents whether the execution of +//! the task is successful, and its internal semaphore is used to synchronously obtain +//! the output of the predecessor task as the input of this task. -use anymap::{CloneAny, Map}; +use std::{ + slice::Iter, + sync::atomic::{AtomicBool, AtomicPtr, Ordering}, +}; -pub type DMap = Map; +use anymap2::{any::CloneAnySendSync, Map}; +use tokio::sync::Semaphore; -/// Describe task's running result -pub struct ExecState { - /// The execution succeed or not - success: bool, - /// Return value of the execution. - retval: Retval, +type Content = Map; + +/// Describe task's running result. +#[derive(Debug)] +pub(crate) struct ExecState { + /// The execution succeed or not. + success: AtomicBool, + /// Output produced by a task. + output: AtomicPtr, + /// Task output identified by id. + tid: usize, + /// The semaphore is used to control the synchronous blocking of subsequent tasks to obtain the + /// execution results of this task. + /// When a task is successfully executed, the permits inside the semaphore will be increased to + /// n (n represents the number of successor tasks of this task or can also be called the output + /// of the node), which means that the output of the task is available, and then each successor + /// The task will obtain a permits synchronously (the permit will not be returned), which means + /// that the subsequent task has obtained the execution result of this task. + semaphore: Semaphore, } -/// Task's return value -pub struct Retval(Option); +/// Output produced by a task. +#[derive(Debug)] +pub struct Output(Option); -/// Task's input value -pub struct Inputval(Vec>); +/// Task's input value. +pub struct Input(Vec); +#[allow(dead_code)] impl ExecState { - /// Get a new [`ExecState`]. - /// - /// `success`: task finish without panic? - /// - /// `retval`: task's return value - pub fn new(success: bool, retval: Retval) -> Self { - Self { success, retval } + /// Construct a new [`ExeState`]. + pub(crate) fn new(task_id: usize) -> Self { + Self { + success: AtomicBool::new(false), + output: AtomicPtr::new(std::ptr::null_mut()), + tid: task_id, + semaphore: Semaphore::new(0), + } } - /// Get [`ExecState`]'s return value. - /// - /// This method will clone [`DMap`] that are stored in [`ExecState`]'s [`Retval`]. - pub fn get_dmap(&self) -> Option { - self.retval.0.clone() + /// After the task is successfully executed, set the execution result. + pub(crate) fn set_output(&self, output: Output) { + self.success.store(true, Ordering::Relaxed); + self.output + .store(Box::leak(Box::new(output)), Ordering::Relaxed); + } + + /// [`Output`] for fetching internal storage. + /// This function is generally not called directly, but first uses the semaphore for synchronization control. + pub(crate) fn get_output(&self) -> Option { + unsafe { self.output.load(Ordering::Relaxed).as_ref().unwrap() } + .0 + .clone() } /// The task execution succeed or not. - /// /// `true` means no panic occurs. - pub fn success(&self) -> bool { - self.success + pub(crate) fn success(&self) -> bool { + self.success.load(Ordering::Relaxed) + } + + /// Use id to indicate the output of which task. + pub(crate) fn tid(&self) -> usize { + self.tid + } + /// The semaphore is used to control the synchronous acquisition of task output results. + /// Under normal circumstances, first use the semaphore to obtain a permit, and then call + /// the `get_output` function to obtain the output. If the current task is not completed + /// (no output is generated), the subsequent task will be blocked until the current task + /// is completed and output is generated. + pub(crate) fn semaphore(&self) -> &Semaphore { + &self.semaphore } } -impl Retval { - #[allow(unused)] - /// Get a new [`Retval`]. - /// - /// Since the return value may be transfered between threads, - /// [`Send`], [`Sync`], [`CloneAny`] is needed. +impl Output { + /// Construct a new [`Output`]. /// - /// # Example - /// ```rust - /// let retval = Retval::new(123); - /// ``` - pub fn new(val: H) -> Self { - let mut map = DMap::new(); + /// Since the return value may be transferred between threads, + /// [`Send`], [`Sync`], [`CloneAnySendSync`] is needed. + pub fn new(val: H) -> Self { + let mut map = Content::new(); assert!(map.insert(val).is_none(), "[Error] map insert fails."); Self(Some(map)) } - /// Get empty [`Retval`]. - /// - /// # Example - /// ```rust - /// let retval = Retval::empty(); - /// ``` + /// Construct an empty [`Output`]. pub fn empty() -> Self { Self(None) } } -impl Inputval { - /// Get a new [`Inputval`], values stored in vector are ordered - /// by that of the given [`TaskWrapper`]'s `rely_list`. - pub fn new(vals: Vec>) -> Self { - Self(vals) - } - - #[allow(unused)] - /// This method get needed input value from [`Inputval`], - /// and, it takes an index as input. - /// - /// When input from only one task's return value, - /// just set index `0`. If from muti-tasks' return values, the index depends on - /// which return value you want. The order of the return values are the same - /// as you defined in [`input_from`] function. - /// - /// # Example - /// ```rust - /// // previous definition of `t3` - /// t3.input_from(&[&t1, &t2]); - /// // then you wanna get input - /// let input_from_t1 = input.get(0); - /// let input_from_t2 = input.get(1); - /// ``` - pub fn get(&mut self, index: usize) -> Option { - if let Some(Some(dmap)) = self.0.get_mut(index) { - dmap.remove() - } else { - None - } +impl Input { + /// Constructs input using output produced by a non-empty predecessor task. + pub fn new(input: Vec) -> Self { + Self(input) } - /// Since [`Inputval`] can contain mult-input values, and it's implemented - /// by [`Vec`] actually, of course it can be turned into a iterater. - pub fn get_iter(&self) -> Iter>> { + /// Since [`Input`] can contain multi-input values, and it's implemented + /// by [`Vec`] actually, of course it can be turned into a iterator. + pub fn get_iter(&self) -> Iter { self.0.iter() } } diff --git a/dagrs/src/task/task.rs b/dagrs/src/task/task.rs deleted file mode 100644 index dd0c2c6c..00000000 --- a/dagrs/src/task/task.rs +++ /dev/null @@ -1,234 +0,0 @@ -use crate::engine::{DagError, EnvVar, RunningError}; - -use super::{Inputval, Retval}; -use deno_core::{serde_json, serde_v8, v8, JsRuntime, RuntimeOptions}; -use lazy_static::lazy_static; -use std::process::Command; -use std::sync::Mutex; - -/// Task Trait. -/// -/// Any struct implements this trait can be added into dagrs. -pub trait TaskTrait { - fn run(&self, input: Inputval, env: EnvVar) -> Retval; -} - -/// Wrapper for task that impl [`TaskTrait`]. -pub struct TaskWrapper { - id: usize, - name: String, - exec_after: Vec, - input_from: Vec, - inner: Box, -} - -impl TaskWrapper { - /// Allocate a new TaskWrapper. - /// - /// # Example - /// ``` - /// let t = TaskWrapper::new(Task{}, "Demo Task") - /// ``` - /// - /// `Task` is a struct that impl [`TaskTrait`]. Since task will be - /// executed in seperated threads, [`send`] and [`sync`] is needed. - /// - /// **Note:** This method will take the ownership of struct that impl [`TaskTrait`]. - pub fn new(task: impl TaskTrait + 'static + Send + Sync, name: &str) -> Self { - TaskWrapper { - id: ID_ALLOCATOR.lock().unwrap().alloc(), - name: name.to_owned(), - exec_after: Vec::new(), - input_from: Vec::new(), - inner: Box::new(task), - } - } - - #[allow(unused)] - /// Tasks that shall be executed before this one. - /// - /// # Example - /// ```rust - /// let mut t1 = TaskWrapper::new(T1{}, "Task 1"); - /// let mut t2 = TaskWrapper::new(T2{}, "Task 2"); - /// t2.exec_after(&[&t1]); - /// ``` - /// In above code, `t1` will be executed before `t2`. - pub fn exec_after(&mut self, relys: &[&TaskWrapper]) { - self.exec_after.extend(relys.iter().map(|t| t.get_id())) - } - - /// Input will come from the given tasks' exec result. - /// - /// # Example - /// ```rust - /// t3.exec_after(&[&t1, &t2, &t4]) - /// t3.input_from(&[&t1, &t2]); - /// ``` - /// - /// In aboving code, t3 will have input from `t1` and `t2`'s return value. - pub fn input_from(&mut self, needed: &[&TaskWrapper]) { - self.input_from.extend(needed.iter().map(|t| t.get_id())) - } - - /// The same as `exec_after`, but input are tasks' ids - /// rather than reference to [`TaskWrapper`]. - pub fn exec_after_id(&mut self, relys: &[usize]) { - self.exec_after.extend(relys) - } - - /// The same as `input_from`, but input are tasks' ids - /// rather than reference to [`TaskWrapper`]. - pub fn input_from_id(&mut self, needed: &[usize]) { - self.input_from.extend(needed) - } - - pub fn get_exec_after_list(&self) -> Vec { - self.exec_after.clone() - } - - pub fn get_input_from_list(&self) -> Vec { - self.input_from.clone() - } - - pub fn get_id(&self) -> usize { - self.id - } - - pub fn get_name(&self) -> String { - self.name.to_owned() - } - - pub fn run(&self, input: Inputval, env: EnvVar) -> Retval { - self.inner.run(input, env) - } -} - -/// IDAllocator for TaskWrapper -struct IDAllocator { - id: usize, -} - -impl IDAllocator { - pub fn alloc(&mut self) -> usize { - self.id += 1; - - // Return values - self.id - 1 - } -} - -lazy_static! { - /// Instance of IDAllocator - static ref ID_ALLOCATOR: Mutex = Mutex::new(IDAllocator { id: 1 }); -} - -/// Can be used to run a script cmd or file. -#[derive(Debug)] -pub struct RunScript { - script: String, - executor: RunType, -} - -/// Run script type, now a script can be run in `sh` or embeded `deno`. -/// -/// **Note** this features is not quite perfect, or rather, need lots of improvements. -#[derive(Debug)] -pub enum RunType { - SH, - DENO, -} - -impl RunScript { - /// Generate a new run script. - /// - /// # Example - /// ``` - /// // `script` can be a commnad - /// let r = RunScript::new("echo Hello", RunType::SH); - /// r.exec(); - /// - /// // or a script path - /// let r = RunScript::new("test/test.sh", RunType::SH); - /// r.exec(); - /// ``` - pub fn new(script: &str, executor: RunType) -> Self { - Self { - script: script.to_owned(), - executor, - } - } - - /// Execute the script. - /// - /// # Example - /// ``` - /// let r = RunScript::new("echo Hello", RunType::SH); - /// r.exec(); - /// ``` - /// If execution succeeds, it returns the result in [`String`] type, or it - /// returns a [`DagError`]. - pub fn exec(&self, input: Option) -> Result { - let res = match self.executor { - RunType::SH => self.run_sh(input), - RunType::DENO => self.run_deno(input), - }; - - res - } - - fn run_sh(&self, input: Option) -> Result { - let mut cmd = format!("{} ", self.script); - if let Some(input) = input { - input - .get_iter() - .map(|input| { - cmd.push_str(if let Some(dmap) = input { - if let Some(str) = dmap.get::() { - str - } else { - "" - } - } else { - "" - }) - }) - .count(); - } - - let res = Command::new("sh") - .arg("-c") - .arg(&cmd) - .output() - .map(|output| format!("{}", String::from_utf8(output.stdout).unwrap())); - - res.map_err(|err| err.into()) - } - - fn run_deno(&self, _input: Option) -> Result { - let script = self.script.clone(); - let mut context = JsRuntime::new(RuntimeOptions { - ..Default::default() - }); - match context.execute_script("", &script) { - Ok(global) => { - let scope = &mut context.handle_scope(); - let local = v8::Local::new(scope, global); - - let deserialized_value = serde_v8::from_v8::(scope, local); - - match deserialized_value { - Ok(value) => Ok(value.to_string()), - Err(err) => Err(DagError::running_error(RunningError::RunScriptFailure( - "?".into(), - format!("Cannot deserialize value: {:?}", err), - ))), - } - } - Err(err) => Err(DagError::running_error(RunningError::RunScriptFailure( - "?".into(), - format!("{:?}", err), - ))), - } - } -} diff --git a/dagrs/src/task/yaml_task.rs b/dagrs/src/task/yaml_task.rs deleted file mode 100644 index 22b990f0..00000000 --- a/dagrs/src/task/yaml_task.rs +++ /dev/null @@ -1,194 +0,0 @@ -use super::{Inputval, Retval, RunScript, RunType, TaskTrait, TaskWrapper}; -use crate::engine::{DagError, YamlFormatError, EnvVar}; -use std::{cell::Cell, collections::HashMap, fs::File, io::Read}; -use yaml_rust::{Yaml, YamlLoader}; - -#[derive(Debug)] -/// Task Struct for YAML file. -struct YamlTaskInner { - /// Running Script - run: RunScript, -} - -/// Task struct for YAML file. -pub struct YamlTask { - /// Task's id in yaml file. - /// - /// Be careful that `yaml_id` is different from [`TaskWrapper`]'s id. - yaml_id: String, - /// Task's name. - name: String, - /// Record tasks' `yaml_id` that shall be executed before this task. - afters: Vec, - /// Record tasks' `yaml_id` that shall give their execution results to this task. - froms: Vec, - /// A field shall be wrapper into [`TaskWrapper`] later. - /// - /// Why [`Cell`] and [`Option`]? Useful in funtion `from_yaml`. - inner: Cell>, -} - -impl TaskTrait for YamlTaskInner { - fn run(&self, input: Inputval, _env: EnvVar) -> Retval { - if let Ok(res) = self.run.exec(Some(input)) { - Retval::new(res) - } else { - Retval::empty() - } - } -} - -impl YamlTask { - /// Parse a task from yaml. - /// - /// # Example - /// ``` - /// let task = Task::parse_one(id, yaml); - /// ``` - /// Here `id` and `yaml` comes from: - /// ``` - /// let yaml_tasks = YamlLoader::load_from_str(&yaml_cont)?; - /// let yaml_tasks = yaml_tasks[0]["dagrs"] - /// .as_hash() - /// .ok_or(DagError::format_error("", FormatErrorMark::StartWordError))?; - /// - /// for(id, yaml) in yaml_tasks{ - /// ... - /// } - /// ``` - fn parse_one(id: &str, info: &Yaml) -> Result { - // Get name first - let name = info["name"] - .as_str() - .ok_or(DagError::format_error(YamlFormatError::NoName( - id.to_owned(), - )))? - .to_owned(); - - // Get run script - let run = &info["run"]; - - let executor = match run["type"].as_str().ok_or(DagError::format_error( - YamlFormatError::RunScriptError(id.into()), - ))? { - "sh" => RunType::SH, - "deno" => RunType::DENO, - _ => { - return Err(DagError::format_error(YamlFormatError::RunScriptError( - id.into(), - ))) - } - }; - - let run_script = run["script"].as_str().ok_or(DagError::format_error( - YamlFormatError::RunScriptError(id.into()), - ))?; - - // afters can be empty - let mut afters = Vec::new(); - if let Some(after_tasks) = info["after"].as_vec() { - after_tasks - .iter() - .map(|task_id| afters.push(task_id.as_str().unwrap().to_owned())) - .count(); - } - - // froms can be empty, too - let mut froms = Vec::new(); - if let Some(from_tasks) = info["from"].as_vec() { - from_tasks - .iter() - .map(|task_id| froms.push(task_id.as_str().unwrap().to_owned())) - .count(); - } - - let inner = Cell::new(Some(YamlTaskInner { - run: RunScript::new(run_script, executor), - })); - - Ok(YamlTask { - yaml_id: id.to_string(), - name, - afters, - froms, - inner, - }) - } - - /// Parse all tasks from yaml file. - /// - /// # Example - /// ``` - /// let tasks = YamlTask::parse_tasks("test/test_dag.yaml")?; - /// ``` - fn parse_tasks(filename: &str) -> Result, DagError> { - let mut yaml_cont = String::new(); - - let mut yaml_file = File::open(filename)?; - yaml_file.read_to_string(&mut yaml_cont)?; - - // Parse Yaml - let yaml_tasks = YamlLoader::load_from_str(&yaml_cont)?; - let yaml_tasks = yaml_tasks[0]["dagrs"] - .as_hash() - .ok_or(DagError::format_error(YamlFormatError::StartWordError))?; - - let mut tasks = Vec::new(); - // Read tasks - for (v, w) in yaml_tasks { - let id = v.as_str().unwrap(); - let task = YamlTask::parse_one(id, w)?; - - tasks.push(task); - } - - Ok(tasks) - } - - /// Parse all tasks from yaml file into format recognized by dagrs. - /// - /// # Example - /// ``` - /// let tasks = YamlTask::from_yaml(filename)?; - /// ``` - /// - /// Used in [`crate::DagEngine`]. - pub fn from_yaml(filename: &str) -> Result, DagError> { - let yaml_tasks = YamlTask::parse_tasks(filename)?; - let mut tasks = Vec::new(); - let mut yid2id = HashMap::new(); - - // Form tasks - for ytask in &yaml_tasks { - let task = TaskWrapper::new( - ytask - .inner - .replace(None) - .expect("[Fatal] Abnormal error occurs."), - &ytask.name, - ); - yid2id.insert(ytask.yaml_id.clone(), task.get_id()); - tasks.push(task); - } - - for (index, ytask) in yaml_tasks.iter().enumerate() { - let afters: Vec = ytask - .afters - .iter() - .map(|after| yid2id.get(after).unwrap_or(&0).to_owned()) - .collect(); - // Task 0 won't exist in normal state, thus this will trigger an RelyTaskIllegal Error later. - - let froms: Vec = ytask - .froms - .iter() - .map(|from| yid2id.get(from).unwrap_or(&0).to_owned()) - .collect(); - - tasks[index].exec_after_id(&afters); - tasks[index].input_from_id(&froms); - } - - Ok(tasks) - } -} diff --git a/dagrs/src/utils/env.rs b/dagrs/src/utils/env.rs new file mode 100644 index 00000000..ac692088 --- /dev/null +++ b/dagrs/src/utils/env.rs @@ -0,0 +1,54 @@ +//! Global environment variables. +//! +//! # Environment variable +//! +//! When multiple tasks are running, they may need to share the same data or read +//! the same configuration information. Environment variables can meet this requirement. +//! Before all tasks run, the user builds a [`EnvVar`] and sets all the environment +//! variables. One [`EnvVar`] corresponds to one dag. All tasks in a job can +//! be shared and immutable at runtime. environment variables. + +use std::collections::HashMap; + +use anymap2::{any::CloneAnySendSync, Map}; + +pub type Variable = Map; + +/// environment variable. +#[derive(Debug,Default)] +pub struct EnvVar { + variables: HashMap, +} + +impl EnvVar { + /// Allocate a new [`EnvVar`]. + pub fn new() -> Self { + Self { + variables: HashMap::new(), + } + } + + #[allow(unused)] + /// Set a global variables. + /// + /// # Example + /// ```rust + /// # let mut env = dagrs::EnvVar::new(); + /// env.set("Hello", "World".to_string()); + /// ``` + pub fn set(&mut self, name: &str, var: H) { + let mut v = Variable::new(); + v.insert(var); + self.variables.insert(name.to_owned(), v); + } + + #[allow(unused)] + /// Get environment variables through keys of type &str. + pub fn get(&self, name: &str) -> Option { + if let Some(content) = self.variables.get(name) { + content.clone().remove() + } else { + None + } + } +} \ No newline at end of file diff --git a/dagrs/src/utils/gen_macro.rs b/dagrs/src/utils/gen_macro.rs new file mode 100644 index 00000000..80690f23 --- /dev/null +++ b/dagrs/src/utils/gen_macro.rs @@ -0,0 +1,25 @@ +/// Macros for generating simple tasks. + +/// # Example +/// +/// ```rust +/// use dagrs::{Dag, Action, Input, EnvVar, Output, RunningError, DefaultTask, gen_task,Task}; +/// use std::sync::Arc; +/// let task = gen_task!("task A", |input, env| { +/// Ok(Output::empty()) +/// }); +/// assert_eq!(task.id(),1); +/// assert_eq!(task.name(),"task A"); +/// ``` +#[macro_export] +macro_rules! gen_task { + ($task_name:expr,$action:expr) => {{ + pub struct SimpleAction; + impl Action for SimpleAction { + fn run(&self, input: Input, env: Arc) -> Result { + $action(input,env) + } + } + DefaultTask::new(SimpleAction, $task_name) + }}; +} diff --git a/dagrs/src/utils/log.rs b/dagrs/src/utils/log.rs new file mode 100644 index 00000000..b2b3c590 --- /dev/null +++ b/dagrs/src/utils/log.rs @@ -0,0 +1,203 @@ +//! logger for dagrs. +//! +//! # The Logger +//! +//! [`Logger`] is a log programming interface provided for users. The framework implements +//! a default logger. If users want to customize the logger, they can +//! implement the [`Logger`] trait. +//! There are five log levels: Debug is the highest level, and Off is the lowest level. +//! Before dagrs is executed, the user should first specify the logging level. The level +//! of the logging function called will be compared with the currently specified log level. +//! If it is greater than the currently specified If the log level is set, the log information +//! will not be recorded, otherwise the record will be printed to the specified location. +//! Logs are generally recorded in two locations, which are printed on the terminal or output +//! to a file, which needs to be specified by the user. + +use std::{ + fmt::Display, + fs::File, + io::Write, + sync::{Arc, Mutex, OnceLock}, +}; + +/// Log level. +#[derive(Clone, Copy, Debug)] +pub enum LogLevel { + Debug, + Info, + Warn, + Error, + Off, +} + +/// Log interface. +pub trait Logger { + /// Returns the current log level. + fn level(&self) -> LogLevel; + /// Record debug information. + fn debug(&self, msg: String); + /// Record info information. + fn info(&self, msg: String); + /// Record warn information. + fn warn(&self, msg: String); + /// Record error information. + fn error(&self, msg: String); +} + +/// Default logger. +struct DefaultLogger { + level: LogLevel, + log_pos: Option>, +} + +impl LogLevel { + /// Used to filter log information. + /// This function may be used when the user defines a logger. This function will compare the + /// log level of the information to be recorded with the level of the current logger. + /// If the function returns false, it means that the log should not be recorded. + pub fn check_level(&self, other: Self) -> bool { + let self_level = Self::map_to_number_level(*self); + let other_level = Self::map_to_number_level(other); + self_level >= other_level + } + + /// In order to facilitate the comparison of log levels, the log levels are mapped to numbers 1~5. + fn map_to_number_level(level: LogLevel) -> usize { + match level { + LogLevel::Debug => 5, + LogLevel::Info => 4, + LogLevel::Warn => 3, + LogLevel::Error => 2, + LogLevel::Off => 1, + } + } +} + +impl DefaultLogger { + fn log(&self, msg: String) { + match self.log_pos { + Some(ref file) => { + let mut file = file.lock().unwrap(); + let _ = writeln!(file, "{}", msg); + } + None => { + println!("{}", msg); + } + } + } +} + +impl Logger for DefaultLogger { + fn level(&self) -> LogLevel { + self.level + } + + fn debug(&self, msg: String) { + self.log(msg) + } + + fn info(&self, msg: String) { + self.log(msg) + } + + fn warn(&self, msg: String) { + self.log(msg) + } + + fn error(&self, msg: String) { + self.log(msg) + } +} + +impl Display for LogLevel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + LogLevel::Debug => write!(f, "Debug"), + LogLevel::Info => write!(f, "Info"), + LogLevel::Warn => write!(f, "warn"), + LogLevel::Error => write!(f, "error"), + LogLevel::Off => write!(f, "off"), + } + } +} + +/// Logger instance. +static LOG: OnceLock> = OnceLock::new(); + +/// Initialize the default logger, the user needs to specify the logging level of the logger, +/// and can also specify the location of the log output, if the log_file parameter is passed in +/// None, the log information will be printed to the terminal, otherwise, the log information +/// will be output to the file. +/// +/// # Example +/// +/// ```rust +/// use dagrs::{log,LogLevel}; +/// log::init_logger(LogLevel::Info,None); +/// ``` +pub fn init_logger(fix_log_level: LogLevel, log_file: Option) { + let logger = match log_file { + Some(file) => DefaultLogger { + level: fix_log_level, + log_pos: Some(Mutex::new(file)), + }, + None => DefaultLogger { + level: fix_log_level, + log_pos: None, + }, + }; + if LOG.set(Arc::new(logger)).is_err() { + println!("Logger has been initialized!"); + } +} + +/// Initialize a custom logger. Users implement the [`Logger`] trait to implement logging logic. +/// For custom loggers, please refer to `custom_log` in the example. +/// /// # Example +/// +/// ```rust +/// use dagrs::{log,LogLevel}; +/// log::init_logger(LogLevel::Info,None); +/// log::info("some message.".to_string()) +/// ``` + +pub fn init_custom_logger(logger: impl Logger + Send + Sync + 'static) { + if LOG.set(Arc::new(logger)).is_err() { + println!("Logger has been initialized!"); + } +} + +/// The following `debug`, `info`, `warn`, and `error` functions are the recording functions +/// provided by the logger for users. + +pub fn debug(msg: String) { + let logger = get_logger(); + if logger.level().check_level(LogLevel::Debug) { + logger.debug(msg); + } +} + +pub fn info(msg: String) { + let logger = get_logger(); + if logger.level().check_level(LogLevel::Info) { + logger.info(msg); + } +} + +pub fn warn(msg: String) { + let logger = get_logger(); + if logger.level().check_level(LogLevel::Warn) { + logger.warn(msg); + } +} + +pub fn error(msg: String) { + let logger = get_logger(); + if logger.level().check_level(LogLevel::Error) { + logger.error(msg); + } +} + +fn get_logger() -> Arc { + LOG.get().expect("Logger is not initialized!").clone() +} diff --git a/dagrs/src/utils/mod.rs b/dagrs/src/utils/mod.rs new file mode 100644 index 00000000..8fd867cd --- /dev/null +++ b/dagrs/src/utils/mod.rs @@ -0,0 +1,14 @@ +//! general tool. +//! +//! # dagrs tool module. +//! +//! This module contains common tools for the program, such as: loggers, environment +//! variables, task generation macros. + +#[macro_use] +pub mod gen_macro; +mod env; +pub mod log; + +pub use self::env::EnvVar; +pub use self::log::{LogLevel,Logger}; \ No newline at end of file diff --git a/dagrs/test/test.sh b/dagrs/test/test.sh deleted file mode 100755 index d470797a..00000000 --- a/dagrs/test/test.sh +++ /dev/null @@ -1 +0,0 @@ -echo "exec sh file success" \ No newline at end of file diff --git a/dagrs/test/test_dag1.yaml b/dagrs/test/test_dag1.yaml deleted file mode 100644 index d6861dcc..00000000 --- a/dagrs/test/test_dag1.yaml +++ /dev/null @@ -1,12 +0,0 @@ -dagrs: - a: - name: "任务1" - after: [b] - run: - type: sh - script: ./test/test.sh - b: - name: "任务2" - run: - type: deno - script: Deno.core.print("Hello!") \ No newline at end of file diff --git a/dagrs/test/test_error2.yaml b/dagrs/test/test_error2.yaml deleted file mode 100644 index c43d5236..00000000 --- a/dagrs/test/test_error2.yaml +++ /dev/null @@ -1,6 +0,0 @@ -a: - name: "任务1" - after: [a] - run: - type: sh - script: echo x \ No newline at end of file diff --git a/dagrs/test/test_loop2.yaml b/dagrs/test/test_loop2.yaml deleted file mode 100644 index 69c09868..00000000 --- a/dagrs/test/test_loop2.yaml +++ /dev/null @@ -1,49 +0,0 @@ -dagrs: - a: - name: "任务1" - after: [b, c] - run: - type: sh - script: echo x - b: - name: "任务2" - after: [c, f, g] - run: - type: sh - script: echo x - c: - name: "任务3" - after: [e, g] - run: - type: sh - script: echo x - d: - name: "任务4" - after: [c, e] - run: - type: sh - script: echo x - e: - name: "任务5" - after: [h] - run: - type: sh - script: echo x - f: - name: "任务6" - after: [g] - run: - type: sh - script: echo x - g: - name: "任务7" - after: [h] - run: - type: sh - script: echo x - h: - name: "任务8" - after: [f] - run: - type: sh - script: echo x \ No newline at end of file diff --git a/dagrs/test/test_value_pass1.txt b/dagrs/test/test_value_pass1.txt deleted file mode 100644 index f599e28b..00000000 --- a/dagrs/test/test_value_pass1.txt +++ /dev/null @@ -1 +0,0 @@ -10 diff --git a/dagrs/test/test_value_pass1.yaml b/dagrs/test/test_value_pass1.yaml deleted file mode 100644 index f227d3db..00000000 --- a/dagrs/test/test_value_pass1.yaml +++ /dev/null @@ -1,13 +0,0 @@ -dagrs: - a: - name: "任务1" - after: [b] - from: [b] - run: - type: sh - script: echo > ./test/test_value_pass1.txt - b: - name: "任务2" - run: - type: deno - script: let a = 1+4; a*2 \ No newline at end of file diff --git a/dagrs/test/test_value_pass2.yaml b/dagrs/test/test_value_pass2.yaml deleted file mode 100644 index b4ba7d99..00000000 --- a/dagrs/test/test_value_pass2.yaml +++ /dev/null @@ -1,13 +0,0 @@ -dagrs: - a: - name: "任务1" - run: - type: sh - script: ls README.md - b: - name: "任务2" - after: [a] - from: [a] - run: - type: sh - script: cat > ./test/test_value_pass2.txt diff --git a/dagrs/test/test_dag2.yaml b/dagrs/tests/config/correct.yaml similarity index 57% rename from dagrs/test/test_dag2.yaml rename to dagrs/tests/config/correct.yaml index 093de8f4..6f8b1c63 100644 --- a/dagrs/test/test_dag2.yaml +++ b/dagrs/tests/config/correct.yaml @@ -1,48 +1,48 @@ dagrs: a: - name: "任务1" - after: [b, c] + name: "Task 1" + after: [ b, c ] run: type: sh script: echo a b: - name: "任务2" - after: [c, f, g] + name: "Task 2" + after: [ c, f, g ] run: type: sh script: echo b c: - name: "任务3" - after: [e, g] + name: "Task 3" + after: [ e, g ] run: type: sh script: echo c d: - name: "任务4" - after: [c, e] + name: "Task 4" + after: [ c, e ] run: type: sh script: echo d e: - name: "任务5" - after: [h] + name: "Task 5" + after: [ h ] run: type: sh script: echo e f: - name: "任务6" - after: [g] + name: "Task 6" + after: [ g ] run: type: deno script: Deno.core.print("f\n") g: - name: "任务7" - after: [h] + name: "Task 7" + after: [ h ] run: type: deno script: Deno.core.print("g\n") h: - name: "任务8" + name: "Task 8" run: type: sh - script: ./test/test.sh \ No newline at end of file + script: echo h \ No newline at end of file diff --git a/dagrs/tests/config/custom_file_task.txt b/dagrs/tests/config/custom_file_task.txt new file mode 100644 index 00000000..0d7129be --- /dev/null +++ b/dagrs/tests/config/custom_file_task.txt @@ -0,0 +1,8 @@ +a,Task a,b c,sh,echo a +b,Task b,c f g,sh,echo b +c,Task c,e g,sh,echo c +d,Task d,c e,sh,echo d +e,Task e,h,sh,echo e +f,Task f,g,deno,Deno.core.print("f\n") +g,Task g,h,deno,Deno.core.print("g\n") +h,Task h,,sh,echo h \ No newline at end of file diff --git a/dagrs/tests/config/empty_file.yaml b/dagrs/tests/config/empty_file.yaml new file mode 100644 index 00000000..e092fa8c --- /dev/null +++ b/dagrs/tests/config/empty_file.yaml @@ -0,0 +1 @@ +# no tasks defined \ No newline at end of file diff --git a/dagrs/tests/config/illegal_content.yaml b/dagrs/tests/config/illegal_content.yaml new file mode 100644 index 00000000..0f9f5602 --- /dev/null +++ b/dagrs/tests/config/illegal_content.yaml @@ -0,0 +1,6 @@ +dagrs: + a: + name:"Task a" + run: + type: sh + script: sh_script.sh \ No newline at end of file diff --git a/dagrs/test/test_loop1.yaml b/dagrs/tests/config/loop_error.yaml similarity index 58% rename from dagrs/test/test_loop1.yaml rename to dagrs/tests/config/loop_error.yaml index d5a33126..b632029d 100644 --- a/dagrs/test/test_loop1.yaml +++ b/dagrs/tests/config/loop_error.yaml @@ -1,31 +1,31 @@ dagrs: a: - name: "任务1" - after: [b, c] + name: "Task 1" + after: [ b, c ] run: type: sh script: echo x b: - name: "任务2" - after: [c] + name: "Task 2" + after: [ c ] run: type: sh script: echo x c: - name: "任务3" - after: [d] + name: "Task 3" + after: [ d ] run: type: sh script: echo x d: - name: "任务4" - after: [e] + name: "Task 4" + after: [ e ] run: type: sh script: echo x e: - name: "任务5" - after: [c] + name: "Task 5" + after: [ c ] run: type: sh script: echo x \ No newline at end of file diff --git a/dagrs/test/test_error4.yaml b/dagrs/tests/config/no_run.yaml similarity index 38% rename from dagrs/test/test_error4.yaml rename to dagrs/tests/config/no_run.yaml index b8763053..dee5d001 100644 --- a/dagrs/test/test_error4.yaml +++ b/dagrs/tests/config/no_run.yaml @@ -1,3 +1,3 @@ dagrs: a: - name: "任务1" \ No newline at end of file + name: "Task 1" \ No newline at end of file diff --git a/dagrs/tests/config/no_script.yaml b/dagrs/tests/config/no_script.yaml new file mode 100644 index 00000000..f795e4a4 --- /dev/null +++ b/dagrs/tests/config/no_script.yaml @@ -0,0 +1,6 @@ +dagrs: + a: + name: "Task 1" + run: + type: sh + \ No newline at end of file diff --git a/dagrs/tests/config/no_start_with_dagrs.yaml b/dagrs/tests/config/no_start_with_dagrs.yaml new file mode 100644 index 00000000..acedaf75 --- /dev/null +++ b/dagrs/tests/config/no_start_with_dagrs.yaml @@ -0,0 +1,47 @@ +a: + name: "Task 1" + after: [ b, c ] + run: + type: sh + script: echo a +b: + name: "Task 2" + after: [ c, f, g ] + run: + type: sh + script: echo b +c: + name: "Task 3" + after: [ e, g ] + run: + type: sh + script: echo c +d: + name: "Task 4" + after: [ c, e ] + run: + type: sh + script: echo d +e: + name: "Task 5" + after: [ h ] + run: + type: sh + script: echo e +f: + name: "Task 6" + after: [ g ] + run: + type: deno + script: Deno.core.print("f\n") +g: + name: "Task 7" + after: [ h ] + run: + type: deno + script: Deno.core.print("g\n") +h: + name: "Task 8" + run: + type: sh + script: sh_script.sh \ No newline at end of file diff --git a/dagrs/tests/config/no_task_name.yaml b/dagrs/tests/config/no_task_name.yaml new file mode 100644 index 00000000..886512e5 --- /dev/null +++ b/dagrs/tests/config/no_task_name.yaml @@ -0,0 +1,5 @@ +dagrs: + a: + run: + type: sh + script: echo "Task 1" \ No newline at end of file diff --git a/dagrs/tests/config/no_type.yaml b/dagrs/tests/config/no_type.yaml new file mode 100644 index 00000000..7d7a4ca3 --- /dev/null +++ b/dagrs/tests/config/no_type.yaml @@ -0,0 +1,4 @@ +dagrs: + a: + name: "Task 1" + run: \ No newline at end of file diff --git a/dagrs/test/test_error3.yaml b/dagrs/tests/config/precursor_not_found.yaml similarity index 60% rename from dagrs/test/test_error3.yaml rename to dagrs/tests/config/precursor_not_found.yaml index 6efab9b1..e43a3c03 100644 --- a/dagrs/test/test_error3.yaml +++ b/dagrs/tests/config/precursor_not_found.yaml @@ -1,7 +1,7 @@ dagrs: a: - name: "任务1" - after: [b] + name: "Task 1" + after: [ b ] run: type: sh script: echo x \ No newline at end of file diff --git a/dagrs/tests/config/script_run_failed.yaml b/dagrs/tests/config/script_run_failed.yaml new file mode 100644 index 00000000..34465541 --- /dev/null +++ b/dagrs/tests/config/script_run_failed.yaml @@ -0,0 +1,48 @@ +dagrs: + a: + name: "Task 1" + after: [ b, c ] + run: + type: sh + script: error_cmd + b: + name: "Task 2" + after: [ c, f, g ] + run: + type: sh + script: error_cmd + c: + name: "Task 3" + after: [ e, g ] + run: + type: sh + script: echo c + d: + name: "Task 4" + after: [ c, e ] + run: + type: sh + script: echo d + e: + name: "Task 5" + after: [ h ] + run: + type: sh + script: echo e + f: + name: "Task 6" + after: [ g ] + run: + type: deno + script: Deno.core.print("f\n") + g: + name: "Task 7" + after: [ h ] + run: + type: deno + script: Deno.core.print("g\n") + h: + name: "Task 8" + run: + type: sh + script: echo h \ No newline at end of file diff --git a/dagrs/test/test_error1.yaml b/dagrs/tests/config/self_loop_error.yaml similarity index 36% rename from dagrs/test/test_error1.yaml rename to dagrs/tests/config/self_loop_error.yaml index d133fc75..5542e1d3 100644 --- a/dagrs/test/test_error1.yaml +++ b/dagrs/tests/config/self_loop_error.yaml @@ -1,12 +1,7 @@ dagrs: a: - # no name - after: [b] - run: - type: sh - script: echo x - b: - name: "任务2" + name: "Task 1" + after: [ a ] run: type: sh script: echo x \ No newline at end of file diff --git a/dagrs/tests/config/unsupported_type.yaml b/dagrs/tests/config/unsupported_type.yaml new file mode 100644 index 00000000..c54e77db --- /dev/null +++ b/dagrs/tests/config/unsupported_type.yaml @@ -0,0 +1,5 @@ +dagrs: + a: + name: "Task 1" + run: + type: python \ No newline at end of file diff --git a/dagrs/tests/dag_job_test.rs b/dagrs/tests/dag_job_test.rs new file mode 100644 index 00000000..6778e637 --- /dev/null +++ b/dagrs/tests/dag_job_test.rs @@ -0,0 +1,126 @@ +use std::sync::Arc; + +///! Some tests of the dag engine. +use dagrs::{Action, Dag, DagError, DefaultTask, EnvVar, Input, Output, RunningError,log,LogLevel}; + +#[test] +fn yaml_task_correct_execute() { + log::init_logger(LogLevel::Info, None); + let mut job = Dag::with_yaml("tests/config/correct.yaml").unwrap(); + assert!(job.start().unwrap()); +} + +#[test] +fn yaml_task_loop_graph() { + log::init_logger(LogLevel::Info, None); + let res = Dag::with_yaml("tests/config/loop_error.yaml") + .unwrap() + .start(); + assert!(matches!(res, Err(DagError::LoopGraph))) +} + +#[test] +fn yaml_task_self_loop_graph() { + log::init_logger(LogLevel::Info, None); + let res = Dag::with_yaml("tests/config/self_loop_error.yaml") + .unwrap() + .start(); + assert!(matches!(res, Err(DagError::LoopGraph))) +} + +#[test] +fn yaml_task_failed_execute() { + log::init_logger(LogLevel::Info, None); + let res = Dag::with_yaml("tests/config/script_run_failed.yaml") + .unwrap() + .start(); + assert!(!res.unwrap()); +} + +macro_rules! generate_task { + ($task:ident($val:expr),$name:expr) => {{ + pub struct $task(usize); + impl Action for $task { + fn run(&self, input: Input, env: Arc) -> Result { + let base = env.get::("base").unwrap(); + let mut sum = self.0; + input + .get_iter() + .for_each(|i| sum += i.get::().unwrap() * base); + Ok(Output::new(sum)) + } + } + DefaultTask::new($task($val), $name) + }}; +} + +#[test] +fn task_loop_graph() { + log::init_logger(LogLevel::Info, None); + let mut a = generate_task!(A(1), "Compute A"); + let mut b = generate_task!(B(2), "Compute B"); + let mut c = generate_task!(C(4), "Compute C"); + a.set_predecessors(&[&b]); + b.set_predecessors(&[&c]); + c.set_predecessors(&[&a]); + + let mut env = EnvVar::new(); + env.set("base", 2usize); + + let mut job = Dag::with_tasks(vec![a, b, c]); + job.set_env(env); + let res = job.start(); + assert!(matches!(res, Err(DagError::LoopGraph))); +} + +#[test] +fn non_job() { + log::init_logger(LogLevel::Info, None); + let tasks: Vec = Vec::new(); + let res = Dag::with_tasks(tasks).start(); + assert!(res.is_err()); + println!("{}", res.unwrap_err()); +} + +struct FailedActionC(usize); + +impl Action for FailedActionC { + fn run(&self, _input: Input, env: Arc) -> Result { + let base = env.get::("base").unwrap(); + Ok(Output::new(base / self.0)) + } +} + +struct FailedActionD(usize); + +impl Action for FailedActionD { + fn run(&self, _input: Input, _env: Arc) -> Result { + Err(RunningError::new("error".to_string())) + } +} + +#[test] +fn task_failed_execute() { + log::init_logger(LogLevel::Info, None); + let a = generate_task!(A(1), "Compute A"); + let mut b = generate_task!(B(2), "Compute B"); + let mut c = DefaultTask::new(FailedActionC(0), "Compute C"); + let mut d = DefaultTask::new(FailedActionD(1), "Compute D"); + let mut e = generate_task!(E(16), "Compute E"); + let mut f = generate_task!(F(32), "Compute F"); + let mut g = generate_task!(G(64), "Compute G"); + + b.set_predecessors(&[&a]); + c.set_predecessors(&[&a]); + d.set_predecessors(&[&a]); + e.set_predecessors(&[&b, &c]); + f.set_predecessors(&[&c, &d]); + g.set_predecessors(&[&b, &e, &f]); + + let mut env = EnvVar::new(); + env.set("base", 2usize); + + let mut job = Dag::with_tasks(vec![a, b, c, d, e, f, g]); + job.set_env(env); + assert!(!job.start().unwrap()); +} diff --git a/dagrs/tests/env_test.rs b/dagrs/tests/env_test.rs new file mode 100644 index 00000000..31f6d88d --- /dev/null +++ b/dagrs/tests/env_test.rs @@ -0,0 +1,44 @@ +use std::sync::Arc; + +use dagrs::EnvVar; + +#[test] +fn env_set_get_test() { + let env = init_env(); + assert_eq!(env.get::("test1"), Some(1usize)); + assert_eq!(env.get::("test2"), None); + assert_eq!(env.get::("test3"), Some("3".to_string())) +} + +#[test] +fn multi_thread_immutable_env_test() { + let env = Arc::new(init_env()); + let mut handles = Vec::new(); + + let env1 = env.clone(); + handles.push(std::thread::spawn(move || { + assert_eq!(env1.get::("test1"), Some(1usize)); + })); + + let env2 = env.clone(); + handles.push(std::thread::spawn(move || { + assert_eq!(env2.get::("test1"), Some(1usize)); + })); + + let env3 = env.clone(); + handles.push(std::thread::spawn(move || { + assert_eq!(env3.get::("test2"), None); + })); + handles + .into_iter() + .for_each(|handle| handle.join().unwrap()); +} + +fn init_env() -> EnvVar { + let mut env = EnvVar::new(); + + env.set("test1", 1usize); + env.set("test2", 2i32); + env.set("test3", "3".to_string()); + env +} diff --git a/dagrs/tests/yaml_parser_test.rs b/dagrs/tests/yaml_parser_test.rs new file mode 100644 index 00000000..41720c74 --- /dev/null +++ b/dagrs/tests/yaml_parser_test.rs @@ -0,0 +1,72 @@ +use dagrs::{FileContentError, Parser, ParserError, YamlParser, YamlTaskError}; + +#[test] +fn file_not_found_test() { + let no_such_file = YamlParser.parse_tasks("./no_such_file.yaml"); + assert!(matches!(no_such_file, Err(ParserError::FileNotFound(_)))); +} + +#[test] +fn illegal_yaml_content() { + let illegal_content = YamlParser.parse_tasks("tests/config/illegal_content.yaml"); + assert!(matches!( + illegal_content, + Err(ParserError::FileContentError( + FileContentError::IllegalYamlContent(_) + )) + )); +} + +#[test] +fn empty_content() { + let empty_content = YamlParser.parse_tasks("tests/config/empty_file.yaml"); + assert!(matches!(empty_content,Err(ParserError::FileContentError(FileContentError::Empty(_))))) +} + +#[test] +fn yaml_no_start_with_dagrs() { + let forget_dagrs = YamlParser.parse_tasks("tests/config/no_start_with_dagrs.yaml"); + assert!(matches!(forget_dagrs,Err(ParserError::YamlTaskError(YamlTaskError::StartWordError)))); +} + +#[test] +fn yaml_task_no_name() { + let no_task_name = YamlParser.parse_tasks("tests/config/no_task_name.yaml"); + assert!(matches!(no_task_name,Err(ParserError::YamlTaskError(YamlTaskError::NoNameAttr(_))))); +} + +#[test] +fn yaml_task_not_found_precursor() { + let not_found_pre = YamlParser.parse_tasks("tests/config/precursor_not_found.yaml"); + assert!(matches!(not_found_pre,Err(ParserError::YamlTaskError(YamlTaskError::NotFoundPrecursor(_))))); +} + +#[test] +fn yaml_task_no_run_config() { + let no_run = YamlParser.parse_tasks("tests/config/no_run.yaml"); + assert!(matches!(no_run,Err(ParserError::YamlTaskError(YamlTaskError::NoRunAttr(_))))); +} + +#[test] +fn yaml_task_no_run_type_config() { + let no_run_type = YamlParser.parse_tasks("tests/config/no_type.yaml"); + assert!(matches!(no_run_type,Err(ParserError::YamlTaskError(YamlTaskError::NoTypeAttr(_))))); +} + +#[test] +fn yaml_task_unsupported_type_config() { + let unsupported_type = YamlParser.parse_tasks("tests/config/unsupported_type.yaml"); + assert!(matches!(unsupported_type,Err(ParserError::YamlTaskError(YamlTaskError::UnsupportedType(_))))); +} + +#[test] +fn yaml_task_no_script_config() { + let script = YamlParser.parse_tasks("tests/config/no_script.yaml"); + assert!(matches!(script,Err(ParserError::YamlTaskError(YamlTaskError::NoScriptAttr(_))))); +} + +#[test] +fn correct_parse() { + let tasks = YamlParser.parse_tasks("tests/config/correct.yaml"); + assert!(tasks.is_ok()); +} -- Gitee